개발 블로그로 돌아가기 릴리스 노트 · iPhone·iPad

SharkTTY 1.8.1: 이제 듣습니다

· Safafish

1.8.1의 App Store 릴리스 노트는 가상 마우스에 대한 네 줄입니다. 그 뒤에 있는 빌드는 아흐레 동안의 작업이자 올해 iPhone·iPad 앱이 겪은 가장 큰 단일 도약입니다. 선택적인 AI 계층을 곁들인 음성 입력, 실제 배열 데이터를 바탕으로 다시 만든 키보드, 세 번째 터미널 프로토콜의 클라이언트, Mac 공유 링크, 그리고 자신의 미래 버전을 견디도록 설계한 동기화 형식. 이 글은 그 긴 버전입니다.

이제 듣습니다

휴대폰에서 터미널을 쓸 때 가장 괴로운 부분은 휴대폰으로 셸 명령을 입력하는 일입니다. 그래서 1.8.1은 약 일주일에 걸쳐 단계별로 만든 음성 입력을 추가했습니다.

첫 단계가 가장 중요하면서 비용이 들지 않는 부분입니다. 마이크를 길게 누르고 말하면 인식이 기기에서 실행됩니다. 무료이고, 오프라인이며, 아무것도 휴대폰을 떠나지 않습니다. 인식된 내용은 음성 보드에 편집 가능한 텍스트로 들어오고, 제대로 되었다 싶을 때 터미널에 삽입합니다 — 그 전이 아니라.

두 번째 단계는 제대로 되지 않았을 때를 위한 것입니다. 녹음은 구간 단위로 보관되므로, 문장 전체를 다시 말하는 대신 한 구간만 잘라 내고 그 부분만 다시 인식할 수 있습니다. 히스토리는 지난 녹음을 보관해 다시 들어 보거나 한 번 더 시도할 수 있게 합니다. iPad에서 음성 보드는 투명 키보드처럼 떠 있고 확대됩니다. 큰 화면에서 엉뚱한 자리에 고정된 패널은 그 자체로 문제이기 때문입니다.

그리고 원한다면, 생각합니다

인식은 단어를 줍니다. 명령에는 단어 이상이 필요합니다 — 순서가 맞는 플래그와, 있어서는 안 될 자리에 없는 공백이 필요합니다. 1.8.1의 AI 계층은 전적으로 선택이며 인식과 완전히 별개입니다.

제공자는 직접 가져오면 됩니다. OpenAI, Claude, Gemini, Kimi, GLM API 형식을 지원하고, 대화 모델과 함께 그 제공자에 있다면 전사 모델도 지정합니다. Claude는 대화 전용이라 오디오 API가 없으므로, 그쪽 녹음은 실패하는 대신 기기 내 인식으로 되돌아갑니다. 제공자 키는 이 기기의 Keychain에 머물며 동기화에서 의도적으로 제외됩니다.

제공자를 연결하면 프롬프트 모드가 말한 내용을 깔끔한 문장이나 바로 실행할 수 있는 명령으로 바꿔 주고, 터미널 문맥 첨부 옵션을 켜면 모델이 답하기 전에 화면에 실제로 무엇이 있는지 볼 수 있습니다. 대화 패널도 있어서, 원래라면 브라우저 탭에서 물어봤을 질문을 세션 옆에서 물어볼 수 있습니다.

API 키를 직접 관리하고 싶지 않은 분들을 위해, 로그인하면 공식 클라우드 모델을 쓸 수 있습니다. 앱 내 구입으로 산 포인트로 비용을 치르며 포인트는 여러 기기에서 공유됩니다. 어느 쪽이든 기기 내 인식은 계속 무료이고, 그것이 기본값입니다.

진짜 배열을 갖춘 키보드

1.8까지 플로팅 키보드는 하드코딩된 미국식 배열 하나였습니다. 1.8.1은 이것을 데이터 기반으로 바꾸고 여섯 가지를 내놓았습니다. 미국 영어, 영국 영어, 일본어 (JIS), 프랑스어 (AZERTY), 독일어 (QWERTZ), 스페인어이며, 보드 선택과 함께 설정에서 전환할 수 있습니다.

데이터 기반 배열에는 하드코딩된 배열에는 없던 테스트가 필요합니다. 이제 배열은 누군가 손으로 입력한 표이기 때문입니다. 그래서 문자 도달 가능성 테스트를 추가했습니다 — 모든 배열에 대해, 있어야 마땅한 모든 문자가 어떤 키 조합으로든 실제로 입력 가능한가? 이 테스트는 곧바로 진짜 버그를 둘 찾아냈습니다. AZERTY에는 *가 아예 없었고, JIS 배열은 fn을 엉뚱한 키에 고정해 F 열이 어긋나 있었습니다. 둘 다 영어로 테스트해서는 아무도 잡아내지 못했을 것입니다.

이제 키 위에는 터미널 문맥과 자주 쓰는 명령에 맞춘 단어 추천기가 받쳐 주는 후보 표시줄이 있고, 마이크는 두 가지 동작과 함께 상단 바로 옮겨 갔습니다. 탭하면 음성 보드로 전환되고, 길게 누르면 눌러서 말하기로 후보 표시줄에 곧장 들어갑니다. 이 길게 누르기 제스처는 보기보다 까다로웠습니다 — SwiftUI의 뻔해 보이는 롱프레스 API는 손가락을 뗀 순간을 알려 주지 못하는데, 이에 대해서는 눌러서 녹음 버튼에 대한 글에서 따로 정리했습니다.

세 번째 길: Eternal Terminal

기차 터널을 지나는 동안에도 세션을 살려 두는 것이 Mosh이고, Mosh는 UDP 위에서 동작합니다. 적지 않은 회사와 호텔 네트워크는 TCP만 통과시키는데, 그러면 그곳 사용자에게는 일반 SSH만 남고 세션 유지는 전혀 없습니다. Eternal Terminal은 같은 문제를 TCP로 풉니다. 그리고 이미 많은 사람이 etserver를 운영하고 있습니다. 그 클라이언트에 대해서는 한 주 전에 글을 썼고, 1.8.1이 그것이 실제로 들어온 빌드입니다.

실험적인 전송 방식이 들어와야 할 순서대로 들어왔습니다. 먼저 다른 무엇도 의존할 수 없는 독립된 Rust 크레이트로, 그다음 플래그 뒤에서 FFI를 통해 Swift 앱에 연결했고, 동작을 확인한 뒤에야 제대로 된 호스트 편집기 UI와 설정 토글, 진짜 원인을 짚어 주는 오류 메시지, 그리고 번역이 붙었습니다. 이어서 실패 경로를 두 차례 검토했는데, 네트워크 클라이언트가 조용히 잘못되기 가장 쉬운 곳이 바로 거기입니다.

기본값은 여전히 꺼짐입니다. 설정에서 ET 전송을 켜면 SSH와 Mosh 옆에 세 번째 연결 방식으로 나타납니다. 미리 감안할 점이 하나 있습니다. Mosh의 UDP와 마찬가지로 ET 데이터 연결은 기기에서 etserver 포트로 곧장 이어지며, 점프 호스트를 거쳐 전달되지 않습니다.

자신의 미래를 견디는 설정

이 기간에 Mac 에이전트가 자체 호스트 목록과 터미널을 갖게 되었고, 그 말은 휴대폰과 Mac이 호스트란 무엇인지에 합의해야 한다는 뜻이었습니다. 1.8.1은 양쪽이 함께 쓰는, 표준 호스트·스크립트 스키마를 담은 공유 패키지와 함께 설정 동기화 클라이언트와 그것을 움직이는 코디네이터를 도입했습니다.

서로 독립적으로 업데이트되는 두 앱이 스키마를 공유하면 특정한 위험이 생기고, 우리도 그것을 만났습니다. 오래된 클라이언트가 한 번도 본 적 없는 값을 디코딩한 뒤, 레코드 전체를 다시 쓰면서 그 값을 파괴하는 것입니다. 해법 — 원본 값을 보존하는 열거형, 알 수 없는 필드를 담는 주머니, 그리고 그 계약을 고정하는 테스트 — 는 전방 호환 Codable에 정리해 두었습니다. 이 빌드에서는 그 패턴을 호스트에 먼저 적용한 뒤, 저장된 데스크톱의 항목별 알 수 없는 필드를 포함해 남은 모든 동기화 모델로 넓혔습니다.

무엇이 동기화되고 무엇이 되지 않는지는 그대로이며 분명합니다. 호스트, 스크립트, 드라이브 매핑, 템플릿, 설정은 계정 아래에서 서버로 동기화됩니다. 비밀번호와 SSH 개인 키, 그 암호 문구는 기기를 절대 벗어나지 않습니다 — iOS Keychain에 머물고, 선택하면 iCloud Keychain을 통해 종단 간으로 이동하며, 서버에는 유출될 것이 없습니다.

다른 사람과 Mac 공유하기

1.8.1은 공유 링크의 뷰어 쪽을 추가했습니다. 나와 공유됨 목록, 페어링 문자열 대신 공유를 통해 연결하는 전송 지원, 그리고 소유자의 승인을 거쳐야 하는 세션 처리입니다. 오류 메시지는 일부러 구체적입니다 — 만료된 링크, 취소된 링크, 동시 접속 자리가 모두 찬 경우, 허용 목록에 없는 계정, 예약한 시간대 밖의 접속이 각각 무엇이 일어났는지 말해 줍니다. 해결 방법이 원인에 따라 달라지는데 “연결할 수 없음”은 아무 쓸모가 없기 때문입니다.

여기 하나 더. 원격 데스크톱의 연결을 끊어도 SSH 연결이 늘 그랬듯 탭은 남습니다. 그래서 처음부터 다시 설정하는 대신 오버레이에서 재연결하면 됩니다.

가상 마우스가 자리를 얻다

App Store 릴리스 노트가 설명하는 부분이 이것입니다. 마우스 버튼은 나눠서 배치할 수 있었고, 유연한 대신 손이 있는 자리에 정확히 있는 법이 없었습니다. 이제는 도킹됩니다. 화면 아래쪽을 따라 놓이는 하나의 패널이 기기를 쥔 위치에 맞춰 좌우로 미끄러지고, 화면 키보드가 열릴 때마다 그 위로 올라섭니다.

도킹은 돋보기의 가장 나쁜 버릇도 고쳤습니다. 자유롭게 떠 있을 때는 화면 가장자리를 피해야 했고, 그래서 예상하지 않은 순간에 움직였으며 확대된 화면이 커서와 어긋났습니다. 도킹하면 커서 바로 위에 자리 잡고 그대로 있습니다. 관련된 수정이 둘 더 있습니다. 손가락을 화면 가장자리 밖으로 미끄러뜨려도 커서가 튀거나 버튼이 눌린 채로 남지 않습니다 — 취소된 터치를 이제 취소된 터치로 처리합니다 — 그리고 ⌘, ⌥, ⌃가 이따금 눌린 채로 남아 다음 입력을 뜻밖의 단축키로 만들던 문제도 없어졌습니다.

다른 앱에서도 들리는 벨

터미널이 벨을 울렸는데 그 세션이 지금 보고 있는 세션이 아니면, 이제 iOS가 알림을 보냅니다. 오래 걸리는 빌드 끝에 printf '\a'를 붙여 두면 끝났을 때 휴대폰이 알려 줍니다. 지금 실제로 보고 있는 세션의 벨은 조용히 넘어가는데, 요령은 바로 그것입니다.

언급할 만한 정리 작업

  • 키보드 작업 마무리 단계의 전면 번역 점검: 639개 키를 확인했고 중복은 하나도 남지 않았습니다.
  • 검토에서 나온 정확성 수정 둘: 디코더 상태의 경쟁 조건, 그리고 호스트가 사라질 때 남겨질 수 있던 Keychain 비밀번호.
  • 1.8에서 시작한 Opus 오디오 경로가 여기서 마무리되었습니다 — 임의의 PCM에서 20 ms 프레임을 만드는 버퍼 인코더, 뷰어 쪽 재생, 그리고 오디오 버퍼 목록에서 인터리브된 PCM을 읽는 문제의 수정입니다.
  • 테스트 대상의 배포 버전을 앱과 같은 iOS 17.0으로 낮췄습니다. 앱이 도는 모든 곳에서 테스트도 돌게 하기 위해서입니다.

이 모두가 무료입니다. 기기 내 음성 인식도 포함해서.

다른 언어로 읽기: English · 中文 · 日本語 · Español · Français · 한국어 · Українська

앱 자체에 대한 자세한 내용은 SharkTTY 가이드에서, 사용자용 짧은 업데이트 안내는 새로운 기능에서 확인할 수 있습니다. 가장 빠른 문의 방법은 피드백 보드입니다. 개발 블로그로 돌아가기.