tcl/tk Observing clipboard - events

I want to execute a function when something gets put on the clipboard (e.g. strg+c).
How can I observe the clipboard in TCL/TK so that i can handle a notify if something gets on it (event driven).
I did some research and the command after is not quite that what i was looking.
proc observeClipboard {} {
set lClipboardContent [clipboard get]
# do something with clipboard content
after 1000 observeClipboard
}
It doesn't worked as expected and also it wouldn't be an event driven (smoother) solution.

The simplest way is probably to always own the clipboard selection.
This has several downsides: You are responsible for the clipboard, and some clipboard contents might be lost, so this is not bullet-proof.
proc readclip {} {
after 50 {
puts [set cnt [clipboard get]]
clipboard clear
clipboard append $cnt
selection own -command readclip -selection CLIPBOARD .
selection handle . [list string range $cnt]
}
}
selection own -command readclip -selection CLIPBOARD .
When readclip is invoked, the new application has requested the ownership over the clipboard, but it does not yet have the ownership, so we wait a bit to let it get it, setup everything etc.
Also note that if more than 1 application does this, both applications "battle" over the ownership of the clipboard, which is a bad thing.

Related

osascript is very slower than Script Editor

First of all, I admit that I'm starting with a new project with JXA (Javascript automation for mac os) without having much understanding about AppleScript.
Currently, I'm trying to run following command using JXA:
Application("System Events").processes.windows.name()
First, I used Script Editor to run it. It worked fine and I got the output quickly enough.
However, according to my use case, since I want to get the output of this code frequently from one of my bash script, I tried to execute it using osascript as follows
osascript -l JavaScript -e 'Application("System Events").processes.windows.name()'
But this time, it took few seconds to print the result in the console.
Now my question is why it takes too much time to execute the same script in osascript compare to Script Editor? Is there any way to optimize the performance of it?
Here's the JXA solution:
var winList = Application("System Events").processes.whose({backgroundOnly: {'=': false} }).windows.name();
var winList2 = winList.reduce(
function(accumulator, currentValue) {
return accumulator.concat(currentValue);
},
[]
);
winList2 = winList2.filter(e => (e !== ""));
winList2.join(',')
There may be a better JavaScript from those JavaScript masters.
This is not exactly the answer to your question, but one issue your JXA script has is that it is pulling in ALL processes (which can be a huge number), when all you probably need is those processes which are visible apps. So, let's start with that.
Here's the AppleScript to get a list of all non-empty window names, in a CSV list on one line, of all visible apps:
tell application "System Events"
set appList to (every application process whose background only is false)
set winList2 to {}
repeat with oApp in appList
set winList to (name of every window in oApp whose name is not "")
set winList2 to winList2 & (items of winList)
end repeat
end tell
set AppleScript's text item delimiters to ","
set winListText to winList2 as text
return winListText
-->All Notes,Keyboard Maestro Editor,macos - osascript is very slower than Script Editor - Stack Overflow - Google Chrome - JMichael,Untitled 2.scpt,Untitled 2
This should not be that hard to convert to JXA, but if you are going to just run it as is in a shell script using osascript, I see no advantage to converting to JXA.
I don't know the nature of your workflow, but if it were me I'd run this as a compiled script file (.scpt), and execute your bash script using the AppleScript do script (or JXA doScript()) command.
It would also be faster if you used a .scpt file with the osascript command.
I will continue to work on this script and convert it to JXA, for my own benefit if not yours.
I hope you find this useful. If not, maybe someone else will.
Questions?

Get Automator app result in external Applescript?

Is there a way to retrieve result of an Automator app script in an external Applescript app (not the Applescript lines in Automator)?
Something like:
tell application "My_Automator_App"
-- suppose My_Automator_App checks the Calendar to see if there some events today
-- "Show Result" in Automator will display a list
get the_Result -- list returned by Automator
end tell
I looked into this a little bit and didn't find a natural means by which AppleScript and Automator applets can communicate, although this doesn't mean one definitely doesn't exist.
In the meantime, you could implement one of a couple of workarounds/hacks that, although a little unseemly in their methods, do achieve the desired result without creating any side issues that would affect the functionality of an applet itself.
1. Use The Clipboard
Append a Copy to Clipboard action at the end of the applet's workflow, or following the action whose result you would wish to be reported.
Retrieving the clipboard from AppleScript is simple:
get the clipboard
This will probably suit return values that are simple text strings or a number. Passing an array of items from an Automator action to the clipboard isn't very reliable, sometimes only allowing access to the first item. However, this can be resolved with a small AppleScript within the workflow to process results arrays properly and convert them into an accessible format, e.g. a comma-delimited string.
However, the clipboard is also capable of storing image data, file references, and other data types, so it will be possible (if not always straightforward) to send those to be retrieved in an AppleScript.
Where possible, strings and numbers are the safest storage types.
2. Write Out To A Temporary File
To avoid using the clipboard as an intermediary, or if you wish the applet to report multiple variables without too much work, then writing the data to a temporary file is a fairly common practice, such as is done in shell scripts when persistant values are needed between multiple executions of the same script.
There's actually a special directory that gets periodically purged so that temporary data files don't accumulate: /tmp. It's hidden in Finder, but you can still create files and delete them as you would any other directory. Files that aren't access for 3 days get purged by the system.
There is a New Text File action that can write text to a file:
Specifying the /tmp directory is most easily done by creating a variable whose value is "/tmp" (without the quotes), and dragging that variable onto the appropriate field.
But my inclination would be to insert an AppleScript, or more suitably, a shell script into the workflow, with which file manipulation becomes easy and more capable.
Calendar Events Example
Using a similar example to the scenario you described, a simple applet that retrieves calendar events might have a workflow that looks like this:
where you can calibrate the first action to isolate the events you want, such as today's events. That action returns a type of object that isn't easily processed by AppleScript, but the second action extracts the relevant data in text format, summarising the list of events that the first action returned.
This is where a temporary file is useful to write out the data to a text file, which can then be retrieved in an AppleScript.
Given this Automator applet saved under the named "CalEvents", this AppleScript makes use of that applet and its result:
property tidEvents : [linefeed, linefeed, "EVENT", space] as text
property tidDetails : {tab, " to "}
property tid : a reference to my text item delimiters
run application id "com.apple.automator.CalEvents"
set tid's contents to tidEvents
set EventsSummary to read POSIX file "/tmp/EventsSummary.txt"
set EventsList to the EventsSummary's text items
set [[n], EventsList] to [it, rest] of EventsList
set n to n's last word as number
EventsList -- The final list of events from first to last
Upon its first run, the applet requires consent to access your calendar information, which only needs to be done once and will make the above script appear to fail. Once authorised, you can run the script as often as you like to get the most up-to-date content of the /tmp/EventsSummary.txt file.
Each item in the list variable EventsList is a block of text that looks like this (asterisks are my redactions for privacy, as are the address items in curly braces):
4 OF 8
Summary: GP Appointment
Status: none
Date: 07/12/2017 to 07/12/2017
Time: 14:45:00 to 15:45:00
Location: ******** Medical Centre
{Address Line 1}
{Address Line 2}
{County}
{Post Code}
United Kingdom
Notes: 01*** *****9
Each value is separated from the preceding colon by a tab character, which won't be obvious here. Also, as you can tell from the date format and address, these are British-formatted values, but yours will, of course, be whatever they are set as in Calendar.
But since each list item is much the same, extracting details for a particular event will be simple in AppleScript, first by splitting a particular event item into paragraphs, and then splitting a particular paragraph by either a tab or space character (or both) or some preposition that naturally delimits useful bits of text:
set |Event| to some item in the EventsList
set tid's contents to tidDetails
set EventDetails to {title:text item 2 of paragraph 2 ¬
, startTime:text item 2 of paragraph 5 ¬
, EndTime:text item 3 of paragraph 5} of the |Event|
which places the important event details, such as its name and start/end times, in an AppleScript record:
{title:"GP Appointment", startTime:"15:45:00", EndTime:"16:00:00"}

Pass any .txt file to existing AppleScript using Automator

I currently have an AppleScript that reads a hardcoded plain text file, which contains a long list, into a variable, which is then read into another variable as paragraphs. The script works very well for my purposes. This is basically what I am using:
set fileHandler to (read POSIX file "/path/to/my/file.txt")
set newList to paragraphs of fileHandler
repeat with i in newList
# do stuff
end repeat
The omitted script opens a Safari location, invokes JavaScript using i as a variable on that page, writes the result to a new plain text file, closes the Safari window, then repeats. It continues until it reaches the end of the list, then outside the repeat runs a do shell script that cleans up the new text file a bit.
The trouble is that every time I want to run the script using a different list, I have to open the hardcoded file and paste in the list. I'd rather just drop any .txt file on to an Automator application that wraps around my current script.
I've tried to use "Combine Text Files" and I feel like I get pretty close but I can't quite pass in the contents of the .txt file the way I'd like. I can't pass it in the same, unless I am doing something wrong with it. And "Get Value of Variable"/"Set Value of Variable" add on an extra "Text" item to my list, which I don't understand.
Ideally, I'd like to do something like this:
set fileHandler to (read POSIX file arg) -- the dropped text file
set newList to paragraphs of fileHandler
repeat with i in newList
# do stuff
end repeat
...but it doesn't work that way, unfortunately.
I'd really rather not reinvent a whole new script if I can help it. Any suggestions would be great. Thanks.
Automator is actually not needed. Save this code in Script Editor as application (bundle) and drop files onto it.
on open theFiles
repeat with aFile in theFiles
set newList to paragraphs of (read aFile)
repeat with i in newList
# do stuff
end repeat
end repeat
end open

Paste from a file one line (or section) at a time

I am a teacher using Windows and would like to be able to paste short program snippets one after another from a file of examples I have into whatever programming environment I am teaching (e.g. the python IDLE shell or editor). During the lecture I would have IDLE open and then use Ctrl-v to paste line 1 from the file into IDLE, execute & discuss it, then use Ctrl-v to paste line 2 from the file into IDLE, execute & discuss it, then use Ctrl-V to get line 3 into IDLE, and so on ...
I suspect there is some way to do this with a clipboard manager, but haven't found it online.
Being able to paste sections of code instead of just single lines would be really useful as well. The sections of code in the file could be separated by a blank line or some kind of text string indicator.
Having this functionality would allow me to have all my examples ready in a file and then during the lecture have quick access to all the examples one at a time by using Ctrl-v.
The following AutoHotKey script will paste lines from the clipboard, one line at a time, when you press Win+Ctrl+V (on Windows).
If you haven't used AutoHotKey, I highly recommend it.
#^v::
{
originalClipboard := Clipboard
StringSplit, ClipLines, originalClipboard, `n`r
size := StrLen(ClipLines1) + 3
Clipboard = %ClipLines1%
Send ^v`n
Clipboard := SubStr(originalClipboard, size)
return
}
Caveats:
It may not robustly handle line endings--it only works for two-character \r\n endings (the Windows standard). This should be most if not all real-world usages.
AutoHotKey seems to be only for Windows.
After you paste a line, that line is removed from the clipboard so that you are ready for the next one.
It always pastes a whole line at a time, even if the source was a partial line.
When you run out of clipboard lines, it pastes blank lines till you realize it.
It adds a new line by sending a newline. Not sure if this works in all text editors, but it worked in Notepad and a few others I tried.
There may be other nuances that it doesn't handle well.
Unfortunately, I cannot comment, but the great solution by #Patrick only works for me when I add a sleep command - otherwise, the clipboard content gets overwritten before the line is pasted. So if you run into a similar issue, the following version might do it:
#^v::
{
originalClipboard := Clipboard
StringSplit, ClipLines, originalClipboard, `n`r
size := StrLen(ClipLines1) + 3
Clipboard = %ClipLines1%
Send ^v`n
sleep, 500 ;
Clipboard := SubStr(originalClipboard, size)
return
}
Install the MultiLineRun.py script from the IdleX extensions for IDLE (or the whole of IdleX). Idlex is available here: http://idlex.sourceforge.net/.
If you want to automate it:
import win32com.client
shell = win32com.client.Dispatch("WScript.Shell")
shell.AppActivate("Python 2.7.9 Shell")
# or the title of your idle shell window
for line in source.readlines():
# open your source file of examples
# better parse it into groups of commands
# and work each group in a batch
line= line.replace("(","{(}") # sendkeys escape
line= line.replace(")","{)}")
shell.SendKeys(line)
shell.SendKeys("{ENTER}") # for good measure.
"""SendKeys sends a string to the active window.
You can automate reading lines in batches linked to a button press etc
put in delays, copy per char etc
Go to town and make it a mini slide show!
"""

VIM prompting for a variable when running a macro?

I find I waste a lot of time closing and reopening sets of files so I'd like to improve my VIM macro for loading and saving the session to support multiple sessions.
I'd like for it to prompt for a string value, so that I could press my shortcut, then type in for example "foo", and have my macro save the session to .foo (so I also need to do basic string concat on it). Then I'd do the same for the load macro and manage sessions by theme (using MVC framework you tend to have a lot of files to work with).
" Control-S to save and Shift F5 to load
set sessionoptions=tabpages,winpos
map <S-F5> :source ~/.vim/.session<cr>
map <c-s> :mksession! ~/.vim/.session<cr>\| :echo "Session saved."<CR>
I have very little experience of VIM scripting. Is it possible to do this in a one liner, or perhaps a small function?
Thank you.
map <s-f5> :execute "source ".input("session name: ", "~/.vim/session.", "file")<cr>
Enter "foo" to load "session.foo".
Instead, you can also do:
map <s-f5> :source ~/.vim/session.
Note there isn't a <cr>, so you complete the command yourself and press enter — identical typing as above, even down to filename completion.
However, I'd look at calling a function or something else entirely at about this point.
Here's the snippet I have now, in case someone needs something similar (no need to vote). It saves sessions under .session.xyz which are also excluded from my Git project. I like to store them in the Git project folder so they are saved with backups.
I like confirmation echo as well because when you press enter after saving the session otherwise you can't see that anything happened. It's just for feedback.
map <S-F5> :execute "source ".input("Load session: ", "~/Some/Project/.session.", "file")<cr>
map <c-s> :execute "mksession! ".input("Save session: ", "~/Some/Project/.session.", "file")\| :echo "Session saved."<CR>
The file completion makes this very handy, thank you!

Resources