포스트

StreamDeck (12) 맥에 깔린 터미널 골라 명령 버튼 열기

이 시리즈는 내가 직접 쓴 개발기가 아니다. 무엇을 만들지 정해 Claude Code 에게 앱을 만들게 했고, 만든 과정과 쓰인 기술은 Claude Code 가 정리했다. 나는 방향을 정하고, 실기기에서 써 보며 고칠 곳을 찾아 다시 맡겼다. 그래도 글로 남기는 건 공부 때문이다. 아직 내가 아는 프레임워크가 몇 개 없어서, 이 앱에 쓰인 여러 프레임워크를 정리해 두고 나중에 하나씩 찾아보며 익히려고 한다.

3편에서 만든 “명령” 버튼과 “새 터미널” 버튼은 Alacritty로 고정돼 있었다. 앱 위치도 /Applications/Alacritty.app으로 박혀 있었다. 내 맥에서는 잘 돌지만, Alacritty가 없는 맥에서는 두 버튼이 아무것도 못 한다. 사람마다 쓰는 터미널이 다르니 고를 수 있어야 했다.

정한 것은 둘이다.

  • 맥 메뉴에서 터미널을 고른다. 그 맥에 설치된 터미널만 보인다.
  • 이 맥에 없어서 시험할 수 없는 터미널도 목록에 같이 넣는다. 각 터미널의 공식 실행 방법대로 만들고, 확인 못 한 것은 확인 못 했다고 적는다.

맥에 깔린 터미널 찾기

먼저 지원할 터미널을 정했다. 밖에서 “이 폴더에서 이 명령을 실행해”라고 넘길 공식 방법이 있는 터미널만 넣었다. Warp와 Hyper는 그런 방법이 마땅치 않아 뺐다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TerminalApp.swift (DeckKit)
public enum TerminalApp: String, CaseIterable, Sendable {
    case terminal = "com.apple.Terminal"
    case alacritty = "org.alacritty"
    case iterm2 = "com.googlecode.iterm2"
    case ghostty = "com.mitchellh.ghostty"
    case wezterm = "com.github.wez.wezterm"
    case kitty = "net.kovidgoyal.kitty"

    public var bundleId: String { rawValue }

    public var name: String {
        switch self {
        case .terminal: "기본 터미널"
        case .alacritty: "Alacritty"
        // 생략
        }
    }

    /// 새 창에 셸이 이미 떠 있고, AppleScript로 거기에 글을 보내 실행하는 터미널
    public var usesAppleScript: Bool { self == .terminal || self == .iterm2 }
}

RawRepresentable과 CaseIterable로 터미널 목록 만들기

enum TerminalApp: String처럼 enum 뒤에 String을 적으면 case마다 글자 값을 하나씩 붙일 수 있다. 이 값을 raw value라고 하고, Swift가 enum을 RawRepresentable로 만들어 rawValue 속성으로 꺼내 준다. 여기서는 번들 ID(앱마다 하나뿐인 이름표, 2편)를 raw value로 붙여서 bundleId가 곧 rawValue가 된다. CaseIterable을 붙이면 모든 case를 순서대로 담은 allCases가 생긴다. 아래에서 이걸 돌며 설치 여부를 하나씩 본다.

설치 여부는 앱 위치가 아니라 번들 ID로 찾는다. /Applications에 없더라도 ~/Applications처럼 다른 곳에 깐 앱도 찾을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// TerminalSettings.swift (Mac)
enum TerminalSettings {
    static let key = "terminalApp"

    static var saved: String? {
        get { UserDefaults.standard.string(forKey: key) }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }

    /// 지원 목록 중 설치된 터미널과 그 앱 위치
    static func installed() -> [TerminalApp: URL] {
        var found: [TerminalApp: URL] = [:]
        for app in TerminalApp.allCases {
            if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: app.bundleId) { found[app] = url }
        }
        return found
    }
}

UserDefaults와 NSWorkspace로 고른 값과 설치된 터미널 찾기

UserDefaults는 앱 설정 같은 작은 값을 기기에 저장하는 칸이다. string(forKey:)는 그 키에 저장된 글자를 읽고, 없으면 nil을 돌려준다. set(_:forKey:)는 값을 저장한다. 고른 터미널의 번들 ID를 terminalApp 키에 넣어 두고, 맥 앱을 다시 켜도 기억하게 했다.

urlForApplication(withBundleIdentifier:)는 2편에서 앱 버튼을 열 때 쓴 것과 같다. 번들 ID를 주면 그 앱이 있는 위치를 돌려주고, 없으면 nil이다. nil이 아니면 설치된 것으로 보고 위치까지 같이 담아 둔다. 그 위치가 나중에 앱을 띄울 때 쓰인다.

이 맥에서 찾아보니 기본 터미널과 Alacritty만 있었다. 그래서 실기기로 확인할 수 있는 건 이 둘뿐이다.

터미널번들 ID여는 방법이 맥에서
기본 터미널com.apple.TerminalAppleScript확인함
Alacrittyorg.alacritty실행 인자확인함
iTerm2com.googlecode.iterm2AppleScript확인 못 함
Ghosttycom.mitchellh.ghostty실행 인자확인 못 함
WezTermcom.github.wez.wezterm실행 인자확인 못 함
kittynet.kovidgoyal.kitty실행 인자확인 못 함

터미널마다 다른 명령 넘기는 법

터미널은 명령을 받는 방법이 두 갈래로 나뉜다. Alacritty처럼 앱을 띄울 때 실행 인자로 “이 셸을 이렇게 돌려”를 받는 터미널이 있다. 그리고 기본 터미널처럼 실행 인자로는 명령을 못 받고, 대신 AppleScript로 “새 창 열고 이 글을 쳐”라고 시켜야 하는 터미널이 있다.

실행 인자로 넘기는 터미널은 TouchDeck이 open으로 앱을 띄우며 폴더와 셸과 스크립트를 넘기고, 새 창이 그 셸을 처음부터 실행한다. AppleScript 터미널은 TouchDeck이 osascript로 새 창을 열게 한 뒤, 이미 떠 있는 사용자 셸에 cd와 eval 한 줄을 쳐 넣는다

어느 쪽이든 3편에서 맞춘 동작은 그대로 지켜야 했다. 별칭과 Homebrew 경로가 보이는 셸, 정한 폴더에서 시작, 명령 끝의 주석이나 안 닫힌 따옴표가 뒤를 삼키지 않는 eval, 창 남기기 또는 끝나면 창 닫기다.


실행 인자로 넘기는 터미널

Alacritty, Ghostty, WezTerm, kitty는 3편의 Alacritty 방식과 같다. /usr/bin/open -na <앱> --args 뒤에 폴더와 셸을 넘긴다. 셸은 별칭과 Homebrew 경로가 보이는 /bin/zsh -lic(로그인 + 대화형, 3편)이고, 그 셸이 실행할 스크립트는 eval '<명령>'이다. 창을 남기면 뒤에 ; exec zsh를 붙인다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TerminalApp.swift (DeckKit)
public func launch(appPath: String, command: String?, directory: String?, keepOpen: Bool) -> TerminalLaunch {
    let dir = directory.map(AlacrittyCommand.expandTilde) ?? NSHomeDirectory()
    let shell = command.map { ["/bin/zsh", AlacrittyCommand.shellFlags, AlacrittyCommand.script(command: $0, keepOpen: keepOpen)] }
    switch self {
    case .alacritty:
        var args = ["-na", appPath, "--args", "--working-directory", dir]
        if let shell { args += ["-e"] + shell }
        return .open(args)
    case .ghostty:
        var args = ["-na", appPath, "--args", "--working-directory=\(dir)"]
        if let shell { args += ["-e"] + shell }
        return .open(args)
    case .wezterm:
        var args = ["-na", appPath, "--args", "start", "--cwd", dir]
        if let shell { args += ["--"] + shell }
        return .open(args)
    case .kitty:
        var args = ["-na", appPath, "--args", "--directory", dir]
        if let shell { args += shell }
        return .open(args)
    // 생략
    }
}

command가 nil이면 “새 터미널” 버튼이다. 이때는 shell도 nil이라 폴더만 넘기고, 터미널은 평소처럼 빈 창에 기본 셸을 띄운다. 인자는 배열로 넘기므로 셸 따옴표를 따로 신경 쓰지 않아도 된다(3편).

같은 일을 하는데 터미널마다 옵션 이름이 조금씩 다르다. 각 터미널 공식 문서에 적힌 대로 맞췄다.

터미널시작 폴더실행할 프로그램 앞
Alacritty--working-directory <폴더>-e
Ghostty--working-directory=<폴더> (등호로 붙임)-e
WezTermstart --cwd <폴더>--
kitty--directory <폴더>없음(뒤에 그대로 씀)

AppleScript와 osascript란?

AppleScript는 맥 앱에게 일을 시키는 스크립트 언어다. 앱마다 “나는 이런 명령을 받을 수 있다”는 목록을 공개해 두고, 다른 프로그램이 그 목록 안의 명령으로 앱을 움직인다. 식당에 주문서를 넣는 것과 비슷하다. 메뉴판에 있는 것만 주문할 수 있고, 주방에서 실제로 요리하는 건 그 앱이다.

osascript는 AppleScript를 터미널에서 실행하는 맥 기본 명령이다. 3편에서 소리 크기를 바꿀 때 osascript -e "set volume output volume 50"처럼 썼다. -e 하나에 스크립트 한 줄을 넣고, 여러 줄이면 -e를 여러 번 붙인다.

기본 터미널과 iTerm2가 공개한 목록에는 “새 창 열기”와 “그 창에 글 쓰기”가 있다. 그래서 이 둘은 AppleScript로 연다.


AppleScript로 여는 터미널

이 둘은 새 창을 열면 사용자의 기본 셸이 이미 떠 있다. 기본 셸은 로그인 + 대화형으로 뜨니 별칭과 Homebrew 경로도 이미 잡혀 있다. 그래서 -lic로 셸을 따로 띄우지 않고, 떠 있는 셸에 한 줄을 쳐 넣는다.

1
2
3
4
5
6
7
8
9
// TerminalApp.swift (DeckKit), launch 안
case .terminal, .iterm2:
    // 새 창의 셸은 사용자의 로그인 셸이라 별칭과 Homebrew 경로가 이미 잡혀 있다
    var text = "cd \(AlacrittyCommand.singleQuoted(dir))"
    if let command {
        text += " && eval \(AlacrittyCommand.singleQuoted(command))"
        if !keepOpen { text += "; exit" }
    }
    return .appleScript(lines: self == .terminal ? Self.terminalScript : Self.itermScript, text: text)

쳐 넣는 글은 cd '<폴더>' && eval '<명령>'이다. &&는 앞이 성공해야 뒤를 실행한다는 뜻이라, 폴더로 못 들어가면 명령도 실행하지 않는다. “명령이 끝나면 창 닫기”를 켠 버튼이면 뒤에 ; exit를 붙여 셸을 끝낸다. “새 터미널” 버튼이면 cd '<폴더>'만 보낸다. 폴더와 명령을 작은따옴표로 감싸는 singleQuoted는 3편에서 만든 것을 그대로 썼다.

iTerm2에 보내는 AppleScript는 이렇다.

1
2
3
4
5
6
7
on run argv
tell application "iTerm"
activate
set newWindow to (create window with default profile)
tell current session of newWindow to write text (item 1 of argv)
end tell
end run
  1. tell application "iTerm"부터 end tell까지가 iTerm2에게 보내는 주문이다.
  2. activate는 iTerm2를 맨 앞으로 가져온다. 꺼져 있으면 켠다.
  3. create window with default profile은 기본 설정으로 새 창을 만든다.
  4. write text는 그 창의 셸에 글을 치고 엔터까지 누른다. 문법은 iTerm2 Scripting Docs에 있다.

기본 터미널은 do script라는 명령 하나가 새 창을 열고 글까지 친다. 다만 꺼져 있을 때 문제가 있어서 아래에서 따로 다룬다.


소스에 글을 이어 붙이면 생기는 일

가장 쉬운 방법은 AppleScript 소스 안에 명령 글자를 그대로 이어 붙이는 것이다. do script " 뒤에 명령을 넣고 "로 닫는 식이다. 그런데 명령 안에 큰따옴표가 있으면 그 따옴표가 소스의 글자 칸을 중간에서 끊어 버린다. 뒤에 남은 글자는 AppleScript 문법으로 읽혀서 오류가 나거나, 운이 나쁘면 다른 명령이 된다. 버튼에 적은 명령이 그대로 실행되지 않는 것이다.

소스에 이어 붙이면 do script "echo "hi""처럼 명령 안의 따옴표가 소스를 끊어 오류가 나거나 다른 명령이 된다. 인자로 따로 넘기면 소스는 늘 do script (item 1 of argv)로 같고, echo "hi"는 짐처럼 따로 넘어가 글자 그대로 창에 들어간다

그래서 소스와 글을 나눴다. 소스는 늘 같은 줄로 두고, 쳐 넣을 글은 osascript를 실행할 때 맨 뒤 인자로 따로 넘긴다. 3편에서 셸 따옴표를 피하려고 인자를 배열로 넘긴 것과 같은 생각이다. 스크립트를 on run argv로 감싸면, 실행할 때 뒤에 붙인 인자들이 argv라는 목록으로 들어온다(Handlers Docs). 스크립트 안에서는 item 1 of argv로 첫 번째 인자를 꺼낸다. 이 값은 처음부터 끝까지 글자일 뿐이라, 따옴표나 역슬래시가 들어 있어도 AppleScript 문법을 건드리지 않는다.

1
2
3
4
5
6
// ActionRunner.swift (Mac)
runScript: { lines, text in
    // 명령 글자는 소스에 넣지 않고 마지막 인자(argv)로 넘긴다
    let status = try runProcess("/usr/bin/osascript", lines.flatMap { ["-e", $0] } + [text])
    if status != 0 { throw NSError(domain: "osascript", code: Int(status)) }
}

flatMap으로 osascript 인자 만들기

flatMap은 목록의 항목마다 작은 목록을 만들고, 그것들을 한 줄로 이어 붙여 돌려준다. 스크립트 줄마다 ["-e", 줄]을 만들어 이었으니 ["-e", "on run argv", "-e", "tell application ...", ...]이 된다. 그 뒤에 쳐 넣을 글 하나를 붙였다. runProcess는 3편에서 Process로 프로그램을 실행하고 끝날 때까지 기다리게 만든 함수다. 돌려주는 값은 종료 코드(3편)이고, 0이 아니면 실패로 본다.

테스트에서도 이걸 확인했다. 명령에 작은따옴표, 큰따옴표, 역슬래시, 한글을 섞어도 보낼 글 하나에 그대로 들어가고, AppleScript 소스에는 명령 글자가 한 자도 없어야 통과한다. 보낼 글을 실제 zsh로 돌려서, 명령 끝에 # 주석이나 안 닫힌 따옴표가 있어도 그 뒤가 계속 실행되는지도 봤다.


자동화 허락이란?

맥은 한 앱이 다른 앱을 마음대로 움직이지 못하게 막아 둔다. 아무 앱이나 터미널에 명령을 쳐 넣을 수 있으면 위험하기 때문이다. 그래서 처음 한 번 “TouchDeck이 터미널을 제어하려고 합니다” 같은 창을 띄워 사용자에게 묻는다. 3편의 손쉬운 사용 권한과 비슷한 문지기인데, 이쪽은 “어떤 앱이 어떤 앱을” 움직이는지를 짝으로 기억한다. 허락한 것은 시스템 설정 > 개인정보 보호 및 보안 > 자동화에서 앱별로 보고 끌 수 있다(Apple 지원 Docs).

이 창에 띄울 설명은 앱이 직접 적어 둬야 한다. 맥 앱의 project.yml에 넣었다.

1
NSAppleEventsUsageDescription: 버튼을 누르면 고른 터미널에 새 창을 열고 명령을 실행하기 위해 사용합니다.

NSAppleEventsUsageDescription은 다른 앱을 AppleScript로 움직이려는 이유를 적는 Info.plist 칸이다. 이 칸이 비어 있으면 허락 창에 이유가 나오지 않는다.

허락을 거절하거나 나중에 끄면 osascript가 실패한다. 이때 버튼은 실패 이유로 “기본 터미널을(를) 열지 못했어요. 시스템 설정 > 개인정보 보호 및 보안 > 자동화에서 TouchDeck을 켜 주세요”를 아이폰에 띄운다. AppleScript로 여는 터미널을 고르면 맥 메뉴에도 “처음 한 번 macOS가 터미널 제어 허락을 물어봐요”를 작게 적어 뒀다. 거절했을 때의 문구는 테스트로 확인했고, 실기기에서 일부러 꺼 보는 것은 아직 확인 못 했다.


꺼져 있던 기본 터미널의 창 두 개

기본 터미널은 do script "<글>" 한 줄이면 새 창을 열고 글을 친다. 그런데 기본 터미널이 꺼져 있을 때 이렇게 하면 창이 두 개 뜬다고 알려져 있다. 앱이 켜지면서 평소처럼 빈 창을 하나 열고, do script가 명령용 새 창을 또 하나 여는 것이다. 버튼 한 번에 빈 창이 하나씩 딸려 나오면 거슬린다.

그래서 꺼져 있었는지를 먼저 보고, 꺼져 있었으면 새 창 대신 켜지면서 생긴 첫 창에 글을 보내게 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
on run argv
set wasRunning to application "Terminal" is running
tell application "Terminal"
activate
if wasRunning then
do script (item 1 of argv)
else
repeat 20 times
if (count of windows) > 0 then exit repeat
delay 0.1
end repeat
do script (item 1 of argv) in window 1
end if
end tell
end run
  1. application "Terminal" is running은 기본 터미널이 지금 켜져 있는지를 참, 거짓으로 알려 준다. activate가 앱을 켜 버리기 전에 먼저 읽어 wasRunning에 담아 둔다.
  2. 켜져 있었으면 do script로 새 창을 연다. 이미 쓰던 창은 건드리지 않는다.
  3. 꺼져 있었으면 activate로 켠 뒤, 첫 창이 생길 때까지 0.1초씩 최대 20번(2초) 기다린다. count of windows가 창 개수다.
  4. do script ... in window 1은 새 창을 만들지 않고 첫 창에 글을 친다.

실기기에서 기본 터미널을 끈 상태와 켠 상태로 버튼을 눌러 봤고, 창은 버튼 한 번에 하나씩 열렸다. 별칭(blogrun)과 Homebrew 명령(pngquant)도 3편의 Alacritty와 똑같이 됐다.

“명령이 끝나면 창 닫기”를 켠 버튼은 끝나면 exit로 창이 닫혔다. 창은 닫혀도 기본 터미널 앱 자체는 켜진 채 남는다. 맥 앱은 마지막 창을 닫아도 꺼지지 않는 게 보통이라 그대로 뒀다. 창이 닫히는지는 기본 터미널 설정의 “셸이 종료될 때” 항목에 달려 있어서, 그 설정을 바꾼 맥이라면 “[프로세스 완료됨]”만 남고 안 닫힐 수 있다.


고른 터미널 기억하고 바꾸기

고르는 규칙은 DeckKit에 순수 함수로 두고 테스트부터 썼다.

1
2
3
4
5
6
7
// TerminalApp.swift (DeckKit)
public static func choose(saved: String?, installed: Set<TerminalApp>) -> TerminalChoice {
    if let saved, let app = TerminalApp(rawValue: saved) {
        return installed.contains(app) ? .use(app) : .missing(app)
    }
    return .use(installed.contains(.alacritty) ? .alacritty : .terminal)
}

init(rawValue:)와 Set으로 쓸 터미널 정하기

init(rawValue:)는 raw value로 enum 값을 거꾸로 찾는다. 저장해 둔 번들 ID가 목록에 있으면 그 터미널을, 모르는 글자면 nil을 돌려준다. Set은 순서 없이 겹치지 않는 값을 모은 묶음이고, contains(_:)로 그 안에 있는지 빠르게 본다. 설치된 터미널을 Set으로 받아 “고른 게 깔려 있는가”를 물었다.

규칙은 셋이다.

  • 고른 적이 없으면 Alacritty가 깔려 있을 때 Alacritty, 아니면 기본 터미널이다. 기본 터미널은 모든 맥에 있으니 고를 게 늘 하나는 있다. 원래 Alacritty를 쓰던 내 맥은 아무것도 안 바꿔도 전과 똑같이 돈다.
  • 고른 터미널이 깔려 있으면 그것을 쓴다.
  • 고른 터미널이 지워졌으면 missing이다. 몰래 다른 터미널로 바꿔 열지 않는다. 버튼은 실행하지 않고 “Alacritty을(를) 찾을 수 없어요. 맥 메뉴에서 다른 터미널을 고르세요”를 돌려준다. 내가 고른 걸 앱이 멋대로 바꾸면 어디서 무엇이 열렸는지 헷갈리기 때문이다.

맥 메뉴에는 고르기 칸을 뒀다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// MenuContent.swift (Mac)
// 사용자가 고를 때만 저장한다. 메뉴를 열며 화면을 채우는 것만으로는 바꾸지 않는다
Picker("터미널", selection: Binding(
    get: { terminal },
    set: { app in
        terminal = app
        TerminalSettings.saved = app.bundleId
    }
)) {
    ForEach(terminals, id: \.self) { Text($0.name).tag($0) }
}
// 생략
.onAppear {
    // 터미널은 메뉴를 열 때마다 다시 찾는다(그사이 설치하거나 지웠을 수 있다)
    terminals = TerminalApp.allCases.filter { TerminalSettings.installed()[$0] != nil }
    switch TerminalSettings.resolve() {
    case .ready(let app, _), .missing(let app): terminal = app
    }
}

Picker와 Binding(get:set:)으로 고른 순간에만 저장하기

  1. Picker는 여러 값 중 하나를 고르는 칸이다. 맥 메뉴 안에서는 펼쳐지는 목록으로 보인다. selection에 지금 고른 값을 읽고 쓰는 연결을 넘긴다.
  2. ForEach는 목록의 항목마다 화면 조각을 하나씩 만든다. 설치된 터미널마다 이름 글자를 만들었다. id: \.self는 항목 자체를 구분 기준으로 쓴다는 뜻이다.
  3. tag(_:includeOptional:)는 각 줄에 값을 달아 둔다. 그 줄을 고르면 Picker가 이 값을 selection에 넣는다.
  4. Binding(get:set:)은 읽을 때와 쓸 때 할 일을 직접 정한 연결이다. 읽을 때는 화면의 terminal을 주고, 쓸 때는 화면 값을 바꾸면서 UserDefaults에도 저장한다.

값이 바뀔 때마다 저장하는 방법(onChange)도 있지만 쓰지 않았다. 메뉴를 열 때 onAppear가 기본값(Alacritty)을 화면에 채우는 것도 “값이 바뀐 것”이라 저장이 일어난다. 그러면 한 번도 고른 적 없는데 Alacritty가 고른 값으로 굳는다. Binding의 set은 사람이 목록에서 고를 때만 불리니, 저장도 그때만 된다.

onAppear에서는 메뉴를 열 때마다 설치된 터미널을 다시 찾는다. 메뉴를 연 채로 다른 터미널을 깔거나 지웠을 수 있어서다. 고른 터미널이 지워졌으면 고르기 칸 아래에 “Alacritty이(가) 지워졌어요. 다른 터미널을 고르세요”가 뜬다.

맥 메뉴 아래쪽의 “터미널” 고르기 칸을 펼친 화면이다. 이 맥에는 기본 터미널과 Alacritty만 깔려 있어서 둘만 보이고, 지금은 Alacritty가 골라져 있다.

아이폰 쪽 편집 화면의 “맥에서 Alacritty 새 창이 열리고…” 같은 설명도 “맥에서 고른 터미널”로 바꿨다. 어느 터미널로 열지는 맥 메뉴에서만 정하고, 아이폰과 주고받는 메시지는 그대로라 기능 번호는 올리지 않았다.


테스트

무엇을 실행할지는 DeckKit에서 인자 배열이나 AppleScript와 보낼 글로 만들어 테스트하고, 맥은 그 결과대로 open이나 osascript를 실행만 한다. 맥 테스트는 가짜 설치 확인과 가짜 실행으로 돌렸다. DeckKit 209개, 맥 71개가 통과한다.

무엇테스트
지원 목록bundleIdsMatchSpec
실행 인자 터미널alacrittyLaunchMatchesExisting, argumentTerminalsPassTheShellScript
AppleScript 터미널appleScriptTextKeepsQuotesIntact, appleScriptEmptyWindowOnlyChangesFolder, appleScriptTextStillRunsAfterBrokenCommand
고르기defaultPrefersAlacrittyThenTerminal, savedInstalledIsUsed, savedButRemovedIsMissing, unknownSavedFallsBackToDefault
맥에서 실행missingTerminalFailsWithName, appleScriptTerminalRunsScript, appleScriptFailureExplainsAutomation

alacrittyLaunchMatchesExisting는 바꾸기 전 Alacritty 인자와 한 글자도 다르지 않은지 본다. 터미널을 고를 수 있게 하면서 원래 쓰던 Alacritty 동작이 달라지면 안 됐기 때문이다.


참고자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.