Created
June 2, 2021 12:59
-
-
Save hepcat72/55acfc79ecab53904eb325924de1d78c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--What is this?: This is a pair of scripts to provide a "Service" in any application in macOS that can wrap selected text to a specified line length (with an optional leader string (e.g. a comment character or indent)). Note, it does not remove existing hard-returns in the selected text. | |
--Installation: 1. Save the companion perl script (wrap.pl) on your computer. 2. Create an Automator service and paste this code into a "Run AppleScript" action. 3. Select (at the top of the workflow) "Workflow receives current text in any application". 4. Edit the `wrap_script` variable below to the location of the wrap.pl script. 5. Save. 6. Use the service (described below) and address any permissions issues that pop up. You may need to do this once for every app in which you run the service. | |
--How to use it: 1. Select entire lines of text you want to limit the line length of. 2. Right-click the selection and select this service. 3. Edit the line length, add a leader string if desired (e.g. a comment character such as "#"), and indicate whether the leader string should be pre-pended to the first line (note that the leader is included in the line length determination whether it pre-exists or not). 4. Click OK. You can undo and retry if you make a mistake. | |
--Author: Robert Leach, Scientific Programmer, Research Computing Group, Lewis Sigler Institute for Integrative Genomics, Princeton University, [email protected] | |
--NOTES: | |
--This workflow uses the clipboard to retrieve text. Clipboard contents will be preserved (and if it cannot, a warning will appear and allow you to cancel) | |
on run {input, parameters} | |
set wrap_script to "/Users/yourusername/Scripts/wrap.pl" | |
try | |
set debug to false | |
if input's class is not list then | |
set input to getHighlight(debug) | |
end if | |
set stdin to quoted form of first item of input as text | |
tell application "System Events" | |
--Get the name of the frontmost application so we can bring the front window back into focus after the dialog window goes away (which surprisingly, is not the default behavior) | |
set curProc to (name of first process whose frontmost is true) | |
set {n, leader_str, prepend_leader} to my displayMultiDialog("Hard-wrap Selected Text", "Set the desired line length, leader string, and whether to prepend the leader string below.", {"• Line Length", "• Leader string (E.g. comment character '#')", "• Whether to prepend the leader on the first line (0=no, 1=yes)"}, {"80", "", "1"}) | |
--And this is the only trick I've found to bring any window in an app back into focus | |
do shell script "open -a '" & (curProc as string) & "'" | |
delay 0.2 | |
set cmd_leader to quoted form of leader_str | |
set wrapped_str to (do shell script "echo " & stdin & " | " & wrap_script & " " & n & " " & cmd_leader & " " & prepend_leader) | |
--See if we're in Terminal | |
set isTerminal to ((name of first process where it is frontmost) as string) is equal to "Terminal" | |
if isTerminal is true then | |
display dialog wrapped_str | |
else | |
keystroke wrapped_str | |
end if | |
end tell | |
on error errstr | |
display dialog errstr | |
end try | |
return wrapped_str | |
end run | |
on getHighlight(debug) | |
--Save the current contents of the clipboard | |
try | |
set theSpare to the clipboard --as text | |
on error | |
set response to (display dialog "Warning: The contents of the clipboard will be lost." buttons {"Cancel", "OK"}) | |
if button returned of response is "OK" then | |
set theSpare to "" | |
end if | |
end try | |
set the clipboard to "" | |
--Declare the variable we're going to return | |
set selecTxt to "" | |
tell application "System Events" | |
--Initiate the copy | |
keystroke "c" using {command down} | |
--Wait up to 2 seconds for the copy to finish | |
set done to "no" | |
set waitnum to 0 | |
set waitInterval to 0.02 | |
set maxwaits to 100 | |
--Repeat while the clipboard contents have not changed | |
repeat while done = "no" | |
--Get the contents of the clipboard | |
try | |
set selecTxt to the clipboard as text | |
end try | |
--See if we're done or need to wait | |
if waitnum is equal to maxwaits then | |
set done to "yes" | |
else if selecTxt is equal to "" then | |
delay waitInterval | |
set waitnum to waitnum + 1 | |
else | |
set done to "yes" | |
end if | |
end repeat | |
if debug is true then | |
try | |
display dialog "Copied text: " & (the clipboard as text) | |
end try | |
end if | |
end tell | |
--Restore the original clipboard contents | |
set the clipboard to theSpare --as record | |
if debug is true then | |
try | |
display dialog "The clipboard contents have been restored to " & (the clipboard as text) | |
end try | |
end if | |
--Return the highlighted text | |
return selecTxt | |
end getHighlight | |
on displayMultiDialog(mytitle, myPrompt, valuePrompts, default_vals) | |
return (inputItems for valuePrompts given title:mytitle, prompt:myPrompt, defaults:default_vals) | |
end displayMultiDialog | |
to inputItems for someItems given title:theTitle, prompt:thePrompt, defaults:theDefaults | |
(* | |
displays a dialog for multiple item entry - a carriage return is used between each input item | |
for each item in someItems, a line of text is displayed in the dialog and a line is reserved for the input | |
the number of items returned are padded or truncated to match the number of items in someItems | |
to fit the size of the dialog, items should be limited in length (~30) and number (~15) | |
parameters - someItems [list/integer]: a list or count of items to get from the dialog | |
theTitle [boolean/text]: use a default or the given dialog title | |
thePrompt [boolean/text]: use a default or the given prompt text | |
returns [list]: a list of the input items | |
*) | |
if thePrompt is in {true, false} then -- "with" or "without" prompt | |
if thePrompt then | |
set thePrompt to "Input the following items:" & return & return -- default | |
else | |
set thePrompt to "" | |
end if | |
else -- fix up the prompt a bit | |
set thePrompt to thePrompt & return & return | |
end if | |
if theTitle is in {true, false} then if theTitle then -- "with" or "without" title | |
set theTitle to "Multiple Input Dialog" -- default | |
else | |
set theTitle to "" | |
end if | |
if theDefaults is in {false} then -- "with" or "without" prompt | |
set theDefaults to {} | |
end if | |
set theDefaultCount to (count theDefaults) | |
if class of someItems is integer then -- no item list | |
set {theCount, someItems} to {someItems, ""} | |
if thePrompt is not "" then set thePrompt to text 1 thru -2 of thePrompt | |
else | |
set theCount to (count someItems) | |
end if | |
if theCount is less than 1 then error "inputItems handler: empty input list" | |
set {theItems, theInput} to {{}, {}} | |
if theDefaultCount is greater than theCount then error "inputItems handler: Too many default values" | |
repeat with itemNum from 1 to theCount -- set the number of lines in the input and the defaults | |
if itemNum is greater than theDefaultCount then | |
set the end of theInput to "" | |
if theDefaultCount is greater than 0 then | |
set item itemNum of someItems to (item itemNum of someItems) & " [\"\"]" | |
end if | |
else | |
set the end of theInput to item itemNum of theDefaults as string | |
set item itemNum of someItems to (item itemNum of someItems) & " [\"" & (item itemNum of theDefaults) & "\"]" | |
end if | |
end repeat | |
set {tempTID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, return} | |
set {someItems, theInput} to {someItems as text, theInput as text} | |
set AppleScript's text item delimiters to tempTID | |
set theInput to paragraphs of text returned of (display dialog thePrompt & someItems with title theTitle default answer theInput) | |
repeat with anItem from 1 to theCount -- pad/truncate entered items | |
try | |
set the end of theItems to (item anItem of theInput) | |
on error | |
set the end of theItems to "" | |
end try | |
end repeat | |
return theItems | |
end inputItems |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env perl -w | |
#Author: Robert W. Leach | |
#License: GPL 3.0 | |
#Date: 5/30/2021 | |
my $rawwraplen = defined($ARGV[0]) ? $ARGV[0] : 80; | |
my $leader = defined($ARGV[1]) ? $ARGV[1] : ''; | |
my $prepend = defined($ARGV[2]) ? $ARGV[2] : 0; | |
my $wraplen = $rawwraplen - length($leader); | |
my $first = 1; | |
while(<STDIN>) | |
{ | |
my $wrapped = alignCols([$_],[$wraplen],[0],[0]); | |
foreach my $line (split("\n",$wrapped)) | |
{ | |
print(($prepend || !$first ? $leader : ''),$line,"\n"); | |
$first = 0; | |
} | |
} | |
#This method was extracted from CommandLineInterface.pm (currently in | |
#development) on 5/30/2021 | |
sub alignCols | |
{ | |
my $column_vals = $_[0]; #array - strings may have hard returns | |
my $col_widths = $_[1]; #array - may leave off last col or all but 1 | |
my $gap_widths = $_[2]; #array - starts with the indent size | |
my $wrap_indents = $_[3]; #array - soft-wrap indent sizes | |
#my $term_width = getWindowWidth(); | |
#Validate the gap widths | |
if(!defined($gap_widths)) | |
{$gap_widths = [0,map {1} 1..$#{$column_vals}]} | |
elsif(scalar(@$gap_widths) == 1) | |
{ | |
if(scalar(@$column_vals) > 1) | |
{ | |
my $tmp = $gap_widths->[0]; | |
$gap_widths = [0,map {$tmp} 1..$#{$column_vals}]; | |
} | |
} | |
elsif(scalar(@$gap_widths) == $#{$column_vals}) | |
{unshift(@$gap_widths,0)} | |
elsif(scalar(@$gap_widths) != scalar(@$column_vals)) | |
{ | |
print STDERR ("ERROR: Invalid number of gap widths [", | |
scalar(@$gap_widths),"] versus number of column ", | |
"values: [",scalar(@$column_vals),"]."); | |
return(''); | |
} | |
#Validate the column widths | |
if(scalar(@$column_vals) != scalar(@$col_widths)) | |
{ | |
if($#{$column_vals} == scalar(@$col_widths)) | |
{ | |
#push(@$col_widths,$term_width - sum(@$col_widths,@$gap_widths)); | |
} | |
#elsif(scalar(@$col_widths) == 1) | |
# { | |
# my $tmp = $col_widths->[0]; | |
# $col_widths = [(map {$tmp} 1..$#{$column_vals}), | |
# $term_width - sum(@$col_widths,@$gap_widths)]; | |
# } | |
else | |
{ | |
print STDERR ("ERROR: Invalid number of column widths [", | |
scalar(@$col_widths),"] versus number of column ", | |
"values [",scalar(@$column_vals),"]."); | |
return(''); | |
} | |
if($col_widths->[-1] < 1) | |
{ | |
print STDERR ("WARNING: Width [$col_widths->[-1]] too small for ", | |
"specified column widths [", | |
join(',',(@$col_widths)[0..($#{$col_widths}-1)]), | |
"]."); | |
return(''); | |
} | |
} | |
#Validate the soft-wrap indents | |
if(!defined($wrap_indents)) | |
{$wrap_indents = [map {0} 0..$#{$column_vals}]} | |
elsif(scalar(@$wrap_indents) < scalar(@$column_vals)) | |
{ | |
while(scalar(@$wrap_indents) < scalar(@$column_vals)) | |
{push(@$wrap_indents,0)} | |
} | |
elsif(scalar(@$wrap_indents) > scalar(@$column_vals)) | |
{ | |
print STDERR ("ERROR: Invalid number of soft-wrap indents [", | |
scalar(@$wrap_indents),"] versus number of column ", | |
"values: [",scalar(@$column_vals),"]."); | |
return(''); | |
} | |
my($line,$out); | |
my @remainders = @$column_vals; | |
while(scalar(grep {$_ ne ''} @remainders)) | |
{ | |
$line = ''; | |
foreach my $index (0..$#{$column_vals}) | |
{ | |
my $remainder = $remainders[$index]; | |
my $gap_width = $gap_widths->[$index]; | |
my $col_width = $col_widths->[$index]; | |
my $wrap_indent = $wrap_indents->[$index]; | |
if($index > 0) | |
{ | |
#Append spaces to fill up to the current column | |
$line .= ' ' x (sum((@$col_widths)[0..($index - 1)], | |
(@$gap_widths)[0..($index - 1)]) - | |
length($line)); | |
} | |
#Add the gap | |
$line .= ' ' x $gap_width; | |
if(length($remainder) > $col_width || | |
$remainder !~ /^[^\n]{$col_width}\n./s) | |
{ | |
my $soft_wrap = 1; | |
my $current = substr($remainder,0,$col_width); | |
my $next_char = | |
length($remainder) > $col_width ? | |
substr($remainder,$col_width,1) : ''; | |
my $added_hyphen = 0; | |
if($current =~ /^[^\n]*\n\n/) | |
{ | |
$current =~ s/(?<=\n)\n.*//s; | |
$soft_wrap = 0; | |
} | |
elsif($current =~ /\n/) | |
{ | |
$current =~ s/(?<=\n).*//s; | |
$soft_wrap = 0; | |
} | |
elsif(#The random chop didn't just happened to be a valid spot | |
$next_char ne "\n" && $next_char ne '' && | |
$next_char ne ' ' && | |
!($current =~ /(?<=[a-zA-Z])-$/ && | |
$next_char =~ /[a-zA-Z]/)) | |
{ | |
#$current =~ s/(.*)\b\{lb}\s*\S+/$1/; | |
if(length($current) == $col_width) | |
{ | |
my($dashwrap,$commawrap,$spacewrap); | |
$dashwrap = $commawrap = $spacewrap = $current; | |
#Try to break up the current cell value at the last | |
#dash, if one exists that's between letters of a | |
#reasonable-long word (don't break on really-long words, | |
#which might be aligned DNA or some other block of | |
#characters) | |
$dashwrap =~ s/(.*[a-zA-Z]-)(?=[a-zA-Z])\S{1,20}$/$1/; | |
#Try to break up the current cell value at the last | |
#comma, if one exists and is not followed by any of the | |
#following: close-bracket, comma, quote, colon, dot, or | |
#semicolon | |
my $nowrapcomma = '[\.,\'";:\)\]\}\>]'; | |
$commawrap =~ s/(.*[^,],)(?!$nowrapcomma)\S.*/$1/; | |
#Unless the string ends with spaces | |
unless($spacewrap =~ s/\s+$//) | |
{ | |
#Try to break up the current cell value at the last | |
#space, if one exists that's not followed by | |
#something that looks longer than a real word or a | |
#line- or thought-ending character, like a period, | |
#close-bracket, colon, comma, semicolon, exclamation | |
#point - or event what may be considered a footnote, | |
#like asterisk, up-arrow, or tilde. | |
my $nowrapspc = '[\.,;:\)\]\}\>\!\*\^\~]'; | |
$spacewrap =~ | |
s/(.*\S)\s+((?!$nowrapspc)\S.{0,20})$/$1/; | |
} | |
#If both were trimmed | |
if(length($dashwrap) < length($current) && | |
length($commawrap) < length($current)) | |
{ | |
#Keep the longer one | |
$current = (length($dashwrap) > length($commawrap) ? | |
$dashwrap : $commawrap); | |
} | |
elsif(length($dashwrap) < length($current)) | |
{$current = $dashwrap} | |
else | |
{$current = $commawrap} | |
if(length($spacewrap) < $col_width && | |
(length($current) == $col_width || | |
length($spacewrap) > length($current))) | |
{$current = $spacewrap} | |
} | |
if(length($current) == $col_width && $col_width > 1 && | |
$current !~ /\s$/ && $current !~ /^\s/) | |
{ | |
#Force a dash if valid | |
if($current =~ s/[A-Za-z]$/-/) | |
{$added_hyphen = 1} | |
} | |
} | |
if(length($current) == $col_width && $next_char eq "\n") | |
{$soft_wrap = 0} | |
$current =~ s/[ \t]+$//; | |
#If the line was empty (i.e. the only character was \n) | |
if($current eq '') | |
{ | |
if($remainder =~ /^\n(.*)/s) | |
{$remainder = $1} | |
} | |
else | |
{ | |
my $pattern = $current; | |
chop($pattern) if($added_hyphen); | |
if($remainder =~ /\Q$pattern\E *(.*)/s) | |
{ | |
$remainder = $1; | |
$remainder =~ s/^ *// unless($current =~ /\n/); | |
if($soft_wrap && $wrap_indent && length($remainder) && | |
$remainder ne "\n") | |
{$remainder = (' ' x $wrap_indent) . $remainder} | |
chomp($current); | |
} | |
} | |
if(length($current) == $col_width && $next_char eq "\n") | |
{$remainder =~ s/^\n//} | |
$line .= $current; | |
} | |
else | |
{ | |
$line .= $remainder; | |
$remainder = ''; | |
} | |
if($index == $#{$column_vals}) | |
{$line =~ s/\s*$/\n/s} | |
$remainders[$index] = $remainder; | |
} | |
$out .= $line; | |
} | |
$out =~ s/\s*$/\n/; | |
return($out); | |
} | |
sub sum | |
{ | |
my $sum = 0; | |
$sum += $_ foreach(@_); | |
return($sum); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment