#!/usr/bin/osascript
--------------------------------------------------------------------------------
-- terminator.scpt - v0.6.1 Enhanced "T-1000"
-- Enhanced Terminal session management with smart session reuse and better error reporting
-- Features: Smart session reuse, enhanced error reporting, improved timing, better output formatting
--------------------------------------------------------------------------------
--#region Configuration Properties
property maxCommandWaitTime : 15.0 -- Increased from 10.0 for better reliability
property pollIntervalForBusyCheck : 0.1
property startupDelayForTerminal : 0.7
property minTailLinesOnWrite : 100 -- Increased from 15 for better build log visibility
property defaultTailLines : 100 -- Increased from 30 for better build log visibility
property tabTitlePrefix : "🤖💥 " -- For the window/tab title itself
property scriptInfoPrefix : "Terminator 🤖💥: " -- For messages generated by this script
property projectIdentifierInTitle : "Project: "
property taskIdentifierInTitle : " - Task: "
property enableFuzzyTagGrouping : true
property fuzzyGroupingMinPrefixLength : 4
-- Safe enhanced properties (minimal additions)
property enhancedErrorReporting : true
property verboseLogging : false
--#endregion Configuration Properties
--#region Helper Functions
on isValidPath(thePath)
if thePath is not "" and (thePath starts with "/") then
if not (thePath contains " -") then -- Basic heuristic
return true
end if
end if
return false
end isValidPath
on getPathComponent(thePath, componentIndex)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to "/"
set pathParts to text items of thePath
set AppleScript's text item delimiters to oldDelims
set nonEmptyParts to {}
repeat with aPart in pathParts
if aPart is not "" then set end of nonEmptyParts to aPart
end repeat
if (count nonEmptyParts) = 0 then return ""
try
if componentIndex is -1 then
return item -1 of nonEmptyParts
else if componentIndex > 0 and componentIndex ≤ (count nonEmptyParts) then
return item componentIndex of nonEmptyParts
end if
on error
return ""
end try
return ""
end getPathComponent
on generateWindowTitle(taskTag as text, projectGroup as text)
if projectGroup is not "" then
return tabTitlePrefix & projectIdentifierInTitle & projectGroup & taskIdentifierInTitle & taskTag
else
return tabTitlePrefix & taskTag
end if
end generateWindowTitle
on bufferContainsMeaningfulContentAS(multiLineText, knownInfoPrefix as text, commonShellPrompts as list)
if multiLineText is "" then return false
-- Simple approach: if the trimmed content is substantial and not just our info messages, consider it meaningful
set trimmedText to my trimWhitespace(multiLineText)
if (length of trimmedText) < 3 then return false
-- Check if it's only our script info messages
if trimmedText starts with knownInfoPrefix then
-- If it's ONLY our message and nothing else meaningful, return false
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to linefeed
set textLines to text items of multiLineText
set AppleScript's text item delimiters to oldDelims
set nonInfoLines to 0
repeat with aLine in textLines
set currentLine to my trimWhitespace(aLine as text)
if currentLine is not "" and not (currentLine starts with knownInfoPrefix) then
set nonInfoLines to nonInfoLines + 1
end if
end repeat
-- If we have substantial non-info content, consider it meaningful
return (nonInfoLines > 2)
end if
-- If content doesn't start with our info prefix, likely contains command output
return true
end bufferContainsMeaningfulContentAS
-- Enhanced error reporting helper
on formatErrorMessage(errorType, errorMsg, context)
if enhancedErrorReporting then
set formattedMsg to scriptInfoPrefix & errorType & ": " & errorMsg
if context is not "" then
set formattedMsg to formattedMsg & " (Context: " & context & ")"
end if
return formattedMsg
else
return scriptInfoPrefix & errorMsg
end if
end formatErrorMessage
-- Enhanced logging helper
on logVerbose(message)
if verboseLogging then
log "🔍 " & message
end if
end logVerbose
--#endregion Helper Functions
--#region Main Script Logic (on run)
on run argv
set appSpecificErrorOccurred to false
try
my logVerbose("Starting Terminator v0.6.0 Safe Enhanced")
tell application "System Events"
if not (exists process "Terminal") then
launch application id "com.apple.Terminal"
delay startupDelayForTerminal
end if
end tell
set originalArgCount to count argv
if originalArgCount < 1 then return my usageText()
set projectPathArg to ""
set actualArgsForParsing to argv
if originalArgCount > 0 then
set potentialPath to item 1 of argv
if my isValidPath(potentialPath) then
set projectPathArg to potentialPath
my logVerbose("Detected project path: " & projectPathArg)
if originalArgCount > 1 then
set actualArgsForParsing to items 2 thru -1 of argv
else
return my formatErrorMessage("Argument Error", "Project path \"" & projectPathArg & "\" provided, but no task tag or command specified." & linefeed & linefeed & my usageText(), "")
end if
end if
end if
if (count actualArgsForParsing) < 1 then return my usageText()
set taskTagName to item 1 of actualArgsForParsing
my logVerbose("Task tag: " & taskTagName)
if (length of taskTagName) > 40 or (not my tagOK(taskTagName)) then
set errorMsg to "Task Tag missing or invalid: \"" & taskTagName & "\"." & linefeed & linefeed & ¬
"A 'task tag' (e.g., 'build', 'tests') is a short name (1-40 letters, digits, -, _) " & ¬
"to identify a specific task, optionally within a project session." & linefeed & linefeed
return my formatErrorMessage("Validation Error", errorMsg & my usageText(), "tag validation")
end if
set doWrite to false
set shellCmd to ""
set originalUserShellCmd to ""
set currentTailLines to defaultTailLines
set explicitLinesProvided to false
set argCountAfterTagOrPath to count actualArgsForParsing
if argCountAfterTagOrPath > 1 then
set commandParts to items 2 thru -1 of actualArgsForParsing
if (count commandParts) > 0 then
set lastOfCmdParts to item -1 of commandParts
if my isInteger(lastOfCmdParts) then
set currentTailLines to (lastOfCmdParts as integer)
set explicitLinesProvided to true
my logVerbose("Explicit lines requested: " & currentTailLines)
if (count commandParts) > 1 then
set commandParts to items 1 thru -2 of commandParts
else
set commandParts to {}
end if
end if
end if
if (count commandParts) > 0 then
set originalUserShellCmd to my joinList(commandParts, " ")
my logVerbose("Command detected: " & originalUserShellCmd)
end if
else if argCountAfterTagOrPath = 1 then
-- Only taskTagName was provided after potential projectPathArg
-- This is a read operation by default.
my logVerbose("Read-only operation detected")
end if
if originalUserShellCmd is not "" and (my trimWhitespace(originalUserShellCmd) is not "") then
set doWrite to true
set shellCmd to originalUserShellCmd
else if projectPathArg is not "" and originalUserShellCmd is "" then
-- Path provided, task tag, and empty command string "" OR no command string but lines_to_read was there
set doWrite to true
set shellCmd to "" -- will become 'cd path'
my logVerbose("CD-only operation for path: " & projectPathArg)
else
set doWrite to false
set shellCmd to ""
end if
if currentTailLines < 1 then set currentTailLines to 1
if doWrite and (shellCmd is not "" or projectPathArg is not "") and currentTailLines < minTailLinesOnWrite then
set currentTailLines to minTailLinesOnWrite
my logVerbose("Increased tail lines for write operation: " & currentTailLines)
end if
if projectPathArg is not "" and doWrite then
set quotedProjectPath to quoted form of projectPathArg
if shellCmd is not "" then
set shellCmd to "cd " & quotedProjectPath & " && " & shellCmd
else
set shellCmd to "cd " & quotedProjectPath
end if
my logVerbose("Final command: " & shellCmd)
end if
set derivedProjectGroup to ""
if projectPathArg is not "" then
set derivedProjectGroup to my getPathComponent(projectPathArg, -1)
if derivedProjectGroup is "" then set derivedProjectGroup to "DefaultProject"
my logVerbose("Project group: " & derivedProjectGroup)
end if
set allowCreation to false
if doWrite then
set allowCreation to true
else if explicitLinesProvided then
set allowCreation to true
end if
set effectiveTabTitleForLookup to my generateWindowTitle(taskTagName, derivedProjectGroup)
my logVerbose("Tab title: " & effectiveTabTitleForLookup)
set tabInfo to my ensureTabAndWindow(taskTagName, derivedProjectGroup, allowCreation, effectiveTabTitleForLookup)
if tabInfo is missing value then
if not allowCreation then
set errorMsg to "Terminal session \"" & effectiveTabTitleForLookup & "\" not found." & linefeed & ¬
"To create this session, provide a command (even an empty string \"\" if only 'cd'-ing to a project path), " & ¬
"or specify lines to read (e.g., ... \"" & taskTagName & "\" 1)." & linefeed
if projectPathArg is not "" then
set errorMsg to errorMsg & "Project path was specified as: \"" & projectPathArg & "\"." & linefeed
else
set errorMsg to errorMsg & "If this is for a new project, provide the absolute project path as the first argument." & linefeed
end if
return my formatErrorMessage("Session Error", errorMsg & linefeed & my usageText(), "session lookup")
else
return my formatErrorMessage("Creation Error", "Could not find or create Terminal tab for \"" & effectiveTabTitleForLookup & "\". Check permissions/Terminal state.", "tab creation")
end if
end if
set targetTab to targetTab of tabInfo
set parentWindow to parentWindow of tabInfo
set wasNewlyCreated to wasNewlyCreated of tabInfo
set createdInExistingViaFuzzy to createdInExistingWindowViaFuzzy of tabInfo
my logVerbose("Tab info - new: " & wasNewlyCreated & ", fuzzy: " & createdInExistingViaFuzzy)
set bufferText to ""
set commandTimedOut to false
set tabWasBusyOnRead to false
set previousCommandActuallyStopped to true
set attemptMadeToStopPreviousCommand to false
set identifiedBusyProcessName to ""
set theTTYForInfo to ""
if not doWrite and wasNewlyCreated then
if createdInExistingViaFuzzy then
return scriptInfoPrefix & "New tab \"" & effectiveTabTitleForLookup & "\" created in existing project window and ready."
else
return scriptInfoPrefix & "New tab \"" & effectiveTabTitleForLookup & "\" (in new window) created and ready."
end if
end if
tell application id "com.apple.Terminal"
try
set index of parentWindow to 1
set selected tab of parentWindow to targetTab
if wasNewlyCreated and doWrite then
delay 0.4
else
delay 0.1
end if
if doWrite and shellCmd is not "" then
my logVerbose("Executing command: " & shellCmd)
set canProceedWithWrite to true
if busy of targetTab then
if not wasNewlyCreated or createdInExistingViaFuzzy then
set attemptMadeToStopPreviousCommand to true
set previousCommandActuallyStopped to false
try
set theTTYForInfo to my trimWhitespace(tty of targetTab)
end try
set processesBefore to {}
try
set processesBefore to processes of targetTab
end try
set commonShells to {"login", "bash", "zsh", "sh", "tcsh", "ksh", "-bash", "-zsh", "-sh", "-tcsh", "-ksh", "dtterm", "fish"}
set identifiedBusyProcessName to ""
if (count of processesBefore) > 0 then
repeat with i from (count of processesBefore) to 1 by -1
set aProcessName to item i of processesBefore
if aProcessName is not in commonShells then
set identifiedBusyProcessName to aProcessName
exit repeat
end if
end repeat
end if
my logVerbose("Busy process identified: " & identifiedBusyProcessName)
set processToTargetForKill to identifiedBusyProcessName
set killedViaPID to false
if theTTYForInfo is not "" and processToTargetForKill is not "" then
set shortTTY to text 6 thru -1 of theTTYForInfo
set pidsToKillText to ""
try
set psCommand to "ps -t " & shortTTY & " -o pid,comm | awk '$2 == \"" & processToTargetForKill & "\" {print $1}'"
set pidsToKillText to do shell script psCommand
end try
if pidsToKillText is not "" then
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to linefeed
set pidList to text items of pidsToKillText
set AppleScript's text item delimiters to oldDelims
repeat with aPID in pidList
set aPID to my trimWhitespace(aPID)
if aPID is not "" then
try
do shell script "kill -INT " & aPID
delay 0.3
do shell script "kill -0 " & aPID
try
do shell script "kill -KILL " & aPID
delay 0.2
try
do shell script "kill -0 " & aPID
on error
set previousCommandActuallyStopped to true
end try
end try
on error
set previousCommandActuallyStopped to true
end try
end if
if previousCommandActuallyStopped then
set killedViaPID to true
exit repeat
end if
end repeat
end if
end if
if not previousCommandActuallyStopped and busy of targetTab then
activate
delay 0.5
tell application "System Events" to keystroke "c" using control down
delay 0.6
if not (busy of targetTab) then
set previousCommandActuallyStopped to true
if identifiedBusyProcessName is not "" and (identifiedBusyProcessName is in (processes of targetTab)) then
set previousCommandActuallyStopped to false
end if
end if
else if not busy of targetTab then
set previousCommandActuallyStopped to true
end if
if not previousCommandActuallyStopped then
set canProceedWithWrite to false
end if
else if wasNewlyCreated and not createdInExistingViaFuzzy and busy of targetTab then
delay 0.4
if busy of targetTab then
set attemptMadeToStopPreviousCommand to true
set previousCommandActuallyStopped to false
set identifiedBusyProcessName to "extended initialization"
set canProceedWithWrite to false
else
set previousCommandActuallyStopped to true
end if
end if
end if
if canProceedWithWrite then
-- Clear before write to prevent output truncation (only for reused tabs)
if not wasNewlyCreated then
do script "clear" in targetTab
delay 0.1
end if
do script shellCmd in targetTab
set commandStartTime to current date
set commandFinished to false
repeat while ((current date) - commandStartTime) < maxCommandWaitTime
if not (busy of targetTab) then
set commandFinished to true
exit repeat
end if
delay pollIntervalForBusyCheck
end repeat
if not commandFinished then set commandTimedOut to true
if commandFinished then delay 0.2 -- Increased from 0.1 for better output settling
my logVerbose("Command execution completed, timeout: " & commandTimedOut)
end if
else if not doWrite then
if busy of targetTab then
set tabWasBusyOnRead to true
try
set theTTYForInfo to my trimWhitespace(tty of targetTab)
end try
set processesReading to processes of targetTab
set commonShells to {"login", "bash", "zsh", "sh", "tcsh", "ksh", "-bash", "-zsh", "-sh", "-tcsh", "-ksh", "dtterm", "fish"}
set identifiedBusyProcessName to ""
if (count of processesReading) > 0 then
repeat with i from (count of processesReading) to 1 by -1
set aProcessName to item i of processesReading
if aProcessName is not in commonShells then
set identifiedBusyProcessName to aProcessName
exit repeat
end if
end repeat
end if
my logVerbose("Tab busy during read with: " & identifiedBusyProcessName)
end if
end if
set bufferText to history of targetTab
on error errMsg number errNum
set appSpecificErrorOccurred to true
return my formatErrorMessage("Terminal Error", errMsg, "error " & errNum)
end try
end tell
set appendedMessage to ""
set ttyInfoStringForMessage to ""
if theTTYForInfo is not "" then set ttyInfoStringForMessage to " (TTY " & theTTYForInfo & ")"
if attemptMadeToStopPreviousCommand then
set processNameToReport to "process"
if identifiedBusyProcessName is not "" and identifiedBusyProcessName is not "extended initialization" then
set processNameToReport to "'" & identifiedBusyProcessName & "'"
else if identifiedBusyProcessName is "extended initialization" then
set processNameToReport to "tab's extended initialization"
end if
if previousCommandActuallyStopped then
set appendedMessage to linefeed & scriptInfoPrefix & "Previous " & processNameToReport & ttyInfoStringForMessage & " was interrupted. ---"
else
set appendedMessage to linefeed & scriptInfoPrefix & "Attempted to interrupt previous " & processNameToReport & ttyInfoStringForMessage & ", but it may still be running. New command NOT executed. ---"
end if
end if
if commandTimedOut then
set cmdForMsg to originalUserShellCmd
if projectPathArg is not "" and originalUserShellCmd is not "" then set cmdForMsg to originalUserShellCmd & " (in " & projectPathArg & ")"
if projectPathArg is not "" and originalUserShellCmd is "" then set cmdForMsg to "(cd " & projectPathArg & ")"
set appendedMessage to appendedMessage & linefeed & scriptInfoPrefix & "Command '" & cmdForMsg & "' may still be running. Returned after " & maxCommandWaitTime & "s timeout. ---"
else if tabWasBusyOnRead then
set processNameToReportOnRead to "process"
if identifiedBusyProcessName is not "" then set processNameToReportOnRead to "'" & identifiedBusyProcessName & "'"
set busyProcessInfoString to ""
if identifiedBusyProcessName is not "" then set busyProcessInfoString to " with " & processNameToReportOnRead
set appendedMessage to appendedMessage & linefeed & scriptInfoPrefix & "Tab" & ttyInfoStringForMessage & " was busy" & busyProcessInfoString & " during read. Output may be from an ongoing process. ---"
end if
if appendedMessage is not "" then
if bufferText is "" then
set bufferText to my trimWhitespace(appendedMessage)
else
set bufferText to bufferText & appendedMessage
end if
end if
set tailedOutput to my tailBufferAS(bufferText, currentTailLines)
set finalResult to my trimBlankLinesAS(tailedOutput)
if finalResult is "" then
set effectiveOriginalCmdForMsg to originalUserShellCmd
if projectPathArg is not "" and originalUserShellCmd is "" then
set effectiveOriginalCmdForMsg to "(cd " & projectPathArg & ")"
else if projectPathArg is not "" and originalUserShellCmd is not "" then
set effectiveOriginalCmdForMsg to originalUserShellCmd & " (in " & projectPathArg & ")"
end if
set baseMsgInfo to "Session \"" & effectiveTabTitleForLookup & "\", requested " & currentTailLines & " lines."
set specificAppendedInfo to my trimWhitespace(appendedMessage)
set suffixForReturn to ""
if specificAppendedInfo is not "" then set suffixForReturn to linefeed & specificAppendedInfo
if attemptMadeToStopPreviousCommand and not previousCommandActuallyStopped then
return my formatErrorMessage("Process Error", "Previous command/initialization in session \"" & effectiveTabTitleForLookup & "\"" & ttyInfoStringForMessage & " may not have terminated. New command '" & effectiveOriginalCmdForMsg & "' NOT executed." & suffixForReturn, "process termination")
else if commandTimedOut then
return my formatErrorMessage("Timeout Error", "Command '" & effectiveOriginalCmdForMsg & "' timed out after " & maxCommandWaitTime & "s. No other output. " & baseMsgInfo & suffixForReturn, "command timeout")
else if tabWasBusyOnRead then
return my formatErrorMessage("Busy Error", "Tab for session \"" & effectiveTabTitleForLookup & "\" was busy during read. No other output. " & baseMsgInfo & suffixForReturn, "read busy")
else if doWrite and shellCmd is not "" then
return scriptInfoPrefix & "Command '" & effectiveOriginalCmdForMsg & "' executed in session \"" & effectiveTabTitleForLookup & "\". No output captured."
else
return scriptInfoPrefix & "No meaningful content found in session \"" & effectiveTabTitleForLookup & "\"."
end if
end if
my logVerbose("Returning " & (length of finalResult) & " characters of output")
return finalResult
on error generalErrorMsg number generalErrorNum
if appSpecificErrorOccurred then error generalErrorMsg number generalErrorNum
return my formatErrorMessage("Execution Error", generalErrorMsg, "error " & generalErrorNum)
end try
end run
--#endregion Main Script Logic (on run)
--#region Helper Functions
on ensureTabAndWindow(taskTagName as text, projectGroupName as text, allowCreate as boolean, desiredFullTitle as text)
set wasActuallyCreated to false
set createdInExistingViaFuzzy to false
tell application id "com.apple.Terminal"
try
repeat with w in windows
repeat with tb in tabs of w
try
if custom title of tb is desiredFullTitle then
set selected tab of w to tb
return {targetTab:tb, parentWindow:w, wasNewlyCreated:false, createdInExistingWindowViaFuzzy:false}
end if
end try
end repeat
end repeat
end try
if allowCreate and enableFuzzyTagGrouping and projectGroupName is not "" then
set projectGroupSearchPatternForWindowName to tabTitlePrefix & projectIdentifierInTitle & projectGroupName
try
repeat with w in windows
try
-- Look for any window that contains our project name
if name of w contains projectGroupSearchPatternForWindowName or name of w contains (projectIdentifierInTitle & projectGroupName) then
if not frontmost then activate
delay 0.2
set newTabInGroup to do script "" in w
delay 0.3
set custom title of newTabInGroup to desiredFullTitle
delay 0.2
set selected tab of w to newTabInGroup
return {targetTab:newTabInGroup, parentWindow:w, wasNewlyCreated:true, createdInExistingWindowViaFuzzy:true}
end if
end try
end repeat
end try
end if
-- Enhanced fallback: if no project-specific window found, try to use any existing Terminator window
if allowCreate and enableFuzzyTagGrouping then
try
repeat with w in windows
try
if name of w contains tabTitlePrefix then
-- Found an existing Terminator window, use it for grouping
if not frontmost then activate
delay 0.2
set newTabInGroup to do script "" in w
delay 0.3
set custom title of newTabInGroup to desiredFullTitle
delay 0.2
set selected tab of w to newTabInGroup
return {targetTab:newTabInGroup, parentWindow:w, wasNewlyCreated:true, createdInExistingWindowViaFuzzy:true}
end if
end try
end repeat
end try
end if
if allowCreate then
try
if not frontmost then activate
delay 0.3
set newTabInNewWindow to do script ""
set wasActuallyCreated to true
delay 0.4
set custom title of newTabInNewWindow to desiredFullTitle
delay 0.2
set parentWinOfNew to missing value
try
set parentWinOfNew to window of newTabInNewWindow
on error
if (count of windows) > 0 then set parentWinOfNew to front window
end try
if parentWinOfNew is not missing value then
if custom title of newTabInNewWindow is desiredFullTitle then
set selected tab of parentWinOfNew to newTabInNewWindow
return {targetTab:newTabInNewWindow, parentWindow:parentWinOfNew, wasNewlyCreated:wasActuallyCreated, createdInExistingWindowViaFuzzy:false}
end if
end if
repeat with w_final_scan in windows
repeat with tb_final_scan in tabs of w_final_scan
try
if custom title of tb_final_scan is desiredFullTitle then
set selected tab of w_final_scan to tb_final_scan
return {targetTab:tb_final_scan, parentWindow:w_final_scan, wasNewlyCreated:wasActuallyCreated, createdInExistingWindowViaFuzzy:false}
end if
end try
end repeat
end repeat
return missing value
on error
return missing value
end try
else
return missing value
end if
end tell
end ensureTabAndWindow
on tailBufferAS(txt, n)
set AppleScript's text item delimiters to linefeed
set lst to text items of txt
if (count lst) = 0 then return ""
set startN to (count lst) - (n - 1)
if startN < 1 then set startN to 1
set slice to items startN thru -1 of lst
set outText to slice as text
set AppleScript's text item delimiters to ""
return outText
end tailBufferAS
on lineIsEffectivelyEmptyAS(aLine)
if aLine is "" then return true
set trimmedLine to my trimWhitespace(aLine)
return (trimmedLine is "")
end lineIsEffectivelyEmptyAS
on trimBlankLinesAS(txt)
if txt is "" then return ""
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to {linefeed}
set originalLines to text items of txt
set linesToProcess to {}
repeat with aLineRef in originalLines
set aLine to contents of aLineRef
if my lineIsEffectivelyEmptyAS(aLine) then
set end of linesToProcess to ""
else
set end of linesToProcess to aLine
end if
end repeat
set firstContentLine to 1
repeat while firstContentLine ≤ (count linesToProcess) and (item firstContentLine of linesToProcess is "")
set firstContentLine to firstContentLine + 1
end repeat
set lastContentLine to count linesToProcess
repeat while lastContentLine ≥ firstContentLine and (item lastContentLine of linesToProcess is "")
set lastContentLine to lastContentLine - 1
end repeat
if firstContentLine > lastContentLine then
set AppleScript's text item delimiters to oldDelims
return ""
end if
set resultLines to items firstContentLine thru lastContentLine of linesToProcess
set AppleScript's text item delimiters to linefeed
set trimmedTxt to resultLines as text
set AppleScript's text item delimiters to oldDelims
return trimmedTxt
end trimBlankLinesAS
on trimWhitespace(theText)
set whitespaceChars to {" ", tab}
set newText to theText
repeat while (newText is not "") and (character 1 of newText is in whitespaceChars)
if (length of newText) > 1 then
set newText to text 2 thru -1 of newText
else
set newText to ""
end if
end repeat
repeat while (newText is not "") and (character -1 of newText is in whitespaceChars)
if (length of newText) > 1 then
set newText to text 1 thru -2 of newText
else
set newText to ""
end if
end repeat
return newText
end trimWhitespace
on isInteger(v)
try
v as integer
return true
on error
return false
end try
end isInteger
on tagOK(t)
try
do shell script "/bin/echo " & quoted form of t & " | /usr/bin/grep -E -q '^[A-Za-z0-9_-]+$'"
return true
on error
return false
end try
end tagOK
on joinList(theList, theDelimiter)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to theDelimiter
set theText to theList as text
set AppleScript's text item delimiters to oldDelims
return theText
end joinList
on usageText()
set LF to linefeed
set scriptName to "terminator.scpt"
set exampleProject to "/Users/name/Projects/FancyApp"
set exampleProjectNameForTitle to my getPathComponent(exampleProject, -1)
if exampleProjectNameForTitle is "" then set exampleProjectNameForTitle to "DefaultProject"
set exampleTaskTag to "build_frontend"
set exampleFullCommand to "npm run build"
set generatedExampleTitle to my generateWindowTitle(exampleTaskTag, exampleProjectNameForTitle)
set outText to scriptName & " - v0.6.0 Enhanced \"T-1000\" – AppleScript Terminal helper" & LF & LF
set outText to outText & "Enhancements: Smart session reuse, enhanced error reporting, verbose logging (optional)" & LF & LF
set outText to outText & "Manages dedicated, tagged Terminal sessions, grouped by project path." & LF & LF
set outText to outText & "Core Concept:" & LF
set outText to outText & " 1. For a NEW project, provide the absolute project path FIRST, then task tag, then command:" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"" & exampleTaskTag & "\" \"" & exampleFullCommand & "\"" & LF
set outText to outText & " The script will 'cd' into the project path and run the command." & LF
set outText to outText & " The tab will be titled like: \"" & generatedExampleTitle & "\"" & LF
set outText to outText & " 2. For SUBSEQUENT commands for THE SAME PROJECT, use the project path and task tag:" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"" & exampleTaskTag & "\" \"another_command\"" & LF
set outText to outText & " 3. To simply READ from an existing session (path & tag must identify an existing session):" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"" & exampleTaskTag & "\"" & LF
set outText to outText & " A READ operation on a non-existent tag (without path/command to create) will error." & LF & LF
set outText to outText & "Title Format: \"" & tabTitlePrefix & projectIdentifierInTitle & "<ProjectName>" & taskIdentifierInTitle & "<TaskTag>\"" & LF
set outText to outText & "Or if no project path provided: \"" & tabTitlePrefix & "<TaskTag>\"" & LF & LF
set outText to outText & "Enhanced Features:" & LF
set outText to outText & " • Smart session reuse for same project paths" & LF
set outText to outText & " • Enhanced error reporting with context information" & LF
set outText to outText & " • Optional verbose logging for debugging" & LF
set outText to outText & " • No automatic clearing to prevent interrupting builds" & LF
set outText to outText & " • 100-line default output for better build log visibility" & LF
set outText to outText & " • Automatically 'cd's into project path if provided with a command." & LF
set outText to outText & " • Groups new task tabs into existing project windows if fuzzy grouping enabled." & LF
set outText to outText & " • Interrupts busy processes in reused tabs." & LF & LF
set outText to outText & "Usage Examples:" & LF
set outText to outText & " # Start new project session, cd, run command, get 100 lines:" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"frontend_build\" \"npm run build\" 100" & LF
set outText to outText & " # Create/use 'backend_tests' task tab in the 'FancyApp' project window:" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"backend_tests\" \"pytest\"" & LF
set outText to outText & " # Prepare/create a new session by just cd'ing into project path (empty command):" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"dev_shell\" \"\" 1" & LF
set outText to outText & " # Read from an existing session:" & LF
set outText to outText & " osascript " & scriptName & " \"" & exampleProject & "\" \"frontend_build\" 50" & LF & LF
set outText to outText & "Parameters:" & LF
set outText to outText & " [\"/absolute/project/path\"]: (Optional First Arg) Base path for project. Enables 'cd' and grouping." & LF
set outText to outText & " \"<task_tag_name>\": Required. Specific task name for the tab (e.g., 'build', 'tests')." & LF
set outText to outText & " [\"<shell_command_parts...>\"]: (Optional) Command. If path provided, 'cd path &&' is prepended." & LF
set outText to outText & " Use \"\" for no command (will just 'cd' if path given)." & LF
set outText to outText & " [[lines_to_read]]: (Optional Last Arg) Number of history lines. Default: " & defaultTailLines & "." & LF & LF
set outText to outText & "Notes:" & LF
set outText to outText & " • Provide project path on first use for a project for best window grouping and auto 'cd'." & LF
set outText to outText & " • Ensure Automation permissions for Terminal.app & System Events.app." & LF
set outText to outText & " • Works within Terminal.app's AppleScript limitations for reliable operation." & LF
return outText
end usageText
--#endregion Helper Functions