mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-04 11:05:47 +00:00
774 lines
No EOL
40 KiB
AppleScript
Executable file
774 lines
No EOL
40 KiB
AppleScript
Executable file
#!/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 |