Skip to content

Instantly share code, notes, and snippets.

@hepcat72
Created June 2, 2021 12:59
Show Gist options
  • Save hepcat72/55acfc79ecab53904eb325924de1d78c to your computer and use it in GitHub Desktop.
Save hepcat72/55acfc79ecab53904eb325924de1d78c to your computer and use it in GitHub Desktop.
--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
#!/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