feat(kebab-tui): p9-fb-09 external editor return — terminal restore #68

Merged
altair823 merged 2 commits from feat/p9-fb-09-editor into main 2026-05-03 02:35:46 +00:00
Owner

요약

Search g (citation jump) 후 TUI 화면이 깨지는 도그푸딩 item 7 버그 수정. external editor suspend / restore 의 정확한 시퀀스를 helper 로 묶고, post-resume terminal.clear() 누락을 채움.

변경

  • kebab-tui::editor::with_external_program (new, pub(crate)) — RAII guard 로 atomic 한 suspend → spawn → restore 시퀀스. panic 시에도 raw mode + alt screen 복구.
  • App.pending_editor: Option<EditorRequest> — 키 핸들러는 enqueue 만, run loop 가 TuiTerminal 핸들 들고 spawn.
  • App.force_redraw: bool — editor 종료 후 다음 draw 가 terminal.clear() 우선 실행, 잔상 제거. 향후 다른 use case (config reload 등) 와도 공유.
  • jump_to_citation 시그니처: (&Citation, &str, &Path)(&mut TuiTerminal, &Citation, &str, &Path). pubpub(crate). 기존 raw-mode 토글 + RAII guard 코드 제거하고 helper 호출로 단일화.

테스트

  • 신규: unspawnable_program_surfaces_program_name_in_error, g_key_enqueues_pending_editor_request, g_key_with_no_hits_does_not_enqueue
  • 기존 71 개 TUI 테스트 (14 lib + 17 search + 18 ask + 12 inspect + 10 library) 모두 통과
  • cargo clippy -p kebab-tui --all-targets -- -D warnings clean

문서

  • README kebab tui 행에 Search g 동작 + 자동 redraw 안내
  • HANDOFF entry 추가
  • spec status plannedin_progress

후속

pending_editor queue + with_external_program helper 가 향후 p9-fb-20 (TUI Ask citation jump), p9-fb-09 의 추가 keybinding (o 등) 의 공통 진입점이 됨.

## 요약 Search `g` (citation jump) 후 TUI 화면이 깨지는 도그푸딩 item 7 버그 수정. external editor suspend / restore 의 정확한 시퀀스를 helper 로 묶고, post-resume `terminal.clear()` 누락을 채움. ## 변경 - **`kebab-tui::editor::with_external_program`** (new, `pub(crate)`) — RAII guard 로 atomic 한 suspend → spawn → restore 시퀀스. panic 시에도 raw mode + alt screen 복구. - **`App.pending_editor: Option<EditorRequest>`** — 키 핸들러는 enqueue 만, run loop 가 `TuiTerminal` 핸들 들고 spawn. - **`App.force_redraw: bool`** — editor 종료 후 다음 draw 가 `terminal.clear()` 우선 실행, 잔상 제거. 향후 다른 use case (config reload 등) 와도 공유. - `jump_to_citation` 시그니처: `(&Citation, &str, &Path)` → `(&mut TuiTerminal, &Citation, &str, &Path)`. `pub` → `pub(crate)`. 기존 raw-mode 토글 + RAII guard 코드 제거하고 helper 호출로 단일화. ## 테스트 - 신규: `unspawnable_program_surfaces_program_name_in_error`, `g_key_enqueues_pending_editor_request`, `g_key_with_no_hits_does_not_enqueue` - 기존 71 개 TUI 테스트 (14 lib + 17 search + 18 ask + 12 inspect + 10 library) 모두 통과 - `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean ## 문서 - README `kebab tui` 행에 Search `g` 동작 + 자동 redraw 안내 - HANDOFF entry 추가 - spec status `planned` → `in_progress` ## 후속 `pending_editor` queue + `with_external_program` helper 가 향후 p9-fb-20 (TUI Ask citation jump), p9-fb-09 의 추가 keybinding (`o` 등) 의 공통 진입점이 됨.
altair823 added 1 commit 2026-05-03 02:32:25 +00:00
Search `g` 키 (citation jump) 후 TUI 화면이 깨지는 버그 수정. 도그푸딩
item 7 — `g` 로 vim 띄우고 `:q` 후 복귀하면 이전 frame 의 잔상이 새
draw 위에 겹쳐 보였음.

## 핵심 변경

- **`kebab-tui::editor::with_external_program(&mut TuiTerminal, Command)`**
  helper 추가. suspend / spawn / restore 시퀀스를 RAII guard 로 atomic
  하게 묶어 panic 발생해도 raw mode + alt screen 복구 보장:
    1. LeaveAlternateScreen + Show cursor + disable_raw_mode
    2. Command::status() 로 child 실행
    3. enable_raw_mode + EnterAlternateScreen + Hide cursor +
       `terminal.clear()` ← 이 한 줄이 핵심 fix
- **`App.pending_editor: Option<EditorRequest>`** 추가. 키 핸들러
  (현재 `kebab-tui::search::handle_key_search` 의 `g`) 가 직접 spawn
  하는 대신 EditorRequest 를 enqueue, 실제 spawn 은 run loop 가
  `TuiTerminal` 핸들 in scope 일 때 처리.
- **`App.force_redraw: bool`** ratchet. with_external_program 종료 후
  set, run loop draw 직전 check → terminal.clear() 후 reset. editor
  외 다른 향후 use case (config reload, theme change 등) 도 같은 hook
  사용 가능.

## 가시성 정리

`with_external_program` / `jump_to_citation` 은 `pub(crate)` 로 좁혀짐
— `TuiTerminal` 자체가 module-private (raw mode + alt screen 의 안전
한 lifecycle 은 `Drop` 만 보장) 이므로 외부 caller 는 `App.pending_
editor` enqueue 패턴으로만 spawn 요청 가능. 외부 surface (`build_jump_
command`, `handle_key_search`, `render_search`) 는 그대로.

## 테스트

- `unspawnable_program_surfaces_program_name_in_error` — helper 의 spawn
  실패 경로 (ENOENT) error context 검증
- `g_key_enqueues_pending_editor_request` — `g` on hit → EditorRequest
  enqueue, citation 정보 보존
- `g_key_with_no_hits_does_not_enqueue` — empty hits → no-op
- 기존 17 개 search 테스트 + 14 lib + 18 ask + 12 inspect + 10 library
  모두 통과
- `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean

## 문서

- README: `kebab tui` 행에 Search `g` 동작 + 자동 redraw 안내
- HANDOFF: 2026-05-03 머지 후 발견 entry
- spec status: `planned` → `in_progress`

후속 task (p9-fb-20 의 citation jump in TUI Ask 등) 가 같은
`pending_editor` queue + `with_external_program` helper 위에 build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-03 02:33:29 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — fix 자체는 견고합니다. RAII guard atomic, helper 가 module 밖으로 안 새고, run loop 가 deferred queue 패턴으로 terminal handle 깔끔히 격리. 테스트 3 건도 enqueue / no-op / spawn fail path 골고루.

actionable nit 3 건 inline. 모두 cosmetic / encapsulation 정리 — 동작 변경 없음.

회차 1 — fix 자체는 견고합니다. RAII guard atomic, helper 가 module 밖으로 안 새고, run loop 가 deferred queue 패턴으로 terminal handle 깔끔히 격리. 테스트 3 건도 enqueue / no-op / spawn fail path 골고루. actionable nit 3 건 inline. 모두 cosmetic / encapsulation 정리 — 동작 변경 없음.

pending_editorforce_redraw 둘 다 pub 인데, enqueue 는 같은 crate 안의 키 핸들러만 (e.g. search.rs) 하고 외부 caller (kebab-cli/desktop) 는 App.pending_editor = Some(...) 같은 mutation 을 할 일이 없습니다. encapsulation 관점에서 pub(crate) 가 안전한 default 입니다.

예외: 향후 외부 (예: integration test) 에서 enqueue 검증이 필요하면 그 시점에 pub 으로 풀면 됩니다. 지금 pub 로 두면 외부에서 잘못 mutate 했을 때 invariants ("run loop 가 매 tick 마다 take()") 가 어긋날 수 있습니다.

`pending_editor` 와 `force_redraw` 둘 다 `pub` 인데, enqueue 는 같은 crate 안의 키 핸들러만 (e.g. `search.rs`) 하고 외부 caller (kebab-cli/desktop) 는 `App.pending_editor = Some(...)` 같은 mutation 을 할 일이 없습니다. encapsulation 관점에서 `pub(crate)` 가 안전한 default 입니다. 예외: 향후 외부 (예: integration test) 에서 enqueue 검증이 필요하면 그 시점에 `pub` 으로 풀면 됩니다. 지금 `pub` 로 두면 외부에서 잘못 mutate 했을 때 invariants ("run loop 가 매 tick 마다 take()") 가 어긋날 수 있습니다.
@@ -261,1 +261,4 @@
pub(crate) should_quit: bool,
/// p9-fb-09: deferred external-program request. A pane's key
/// handler enqueues an `EditorRequest` here when the user wants
/// to spawn `$EDITOR` (e.g. Search `g` jumps to a citation in

doc 의 "ratchet incremented every time..." 표현이 type 과 어긋납니다 — force_redrawbool flag 라서 "increment" 가 아니라 "set" / "toggle" 이 정확합니다. 제안:

/// p9-fb-09: when set, the next run-loop draw runs `terminal.clear()`
/// first so any leftover screen content from a suspension (e.g.
/// post-editor) is wiped. Reset back to false after the clear.

실제 동작에 맞고 "counter 라서 monotonic 일 거다" 같은 오해도 막아줍니다.

doc 의 "ratchet incremented every time..." 표현이 type 과 어긋납니다 — `force_redraw` 는 `bool` flag 라서 "increment" 가 아니라 "set" / "toggle" 이 정확합니다. 제안: ``` /// p9-fb-09: when set, the next run-loop draw runs `terminal.clear()` /// first so any leftover screen content from a suspension (e.g. /// post-editor) is wiped. Reset back to false after the clear. ``` 실제 동작에 맞고 "counter 라서 monotonic 일 거다" 같은 오해도 막아줍니다.
@@ -0,0 +134,4 @@
),
"unexpected error kind: {err:?}",
);
}

이 테스트는 이름과 달리 with_external_program 자체를 호출하지 않고 Command::status() 만 직접 호출합니다 — helper 의 with_context(...) wrapper 가 program name 을 넣어주는지 검증하지 못 합니다. 둘 중 하나로 정리하면 좋겠습니다:

  1. 이름과 의도 일치: 테스트 이름을 command_status_returns_not_found_for_missing_program 정도로 바꿔서 "OS 동작 sanity check" 로 명시. helper 내부 동작은 dogfooding 으로 검증한다는 doc comment 추가.
  2. helper 진짜 호출: TuiTerminal 을 mock 하기 어려우면 cfg_attr(test, allow(...)) 로 internal _status_only shim 을 노출해 그것을 테스트. (오버엔지니어링 가능성 있음)

저는 (1) 이 가성비 좋다고 봅니다 — 기존 test 코드는 그대로 두고 이름 + doc 만 손보는 정도.

이 테스트는 이름과 달리 `with_external_program` 자체를 호출하지 않고 `Command::status()` 만 직접 호출합니다 — helper 의 `with_context(...)` wrapper 가 program name 을 넣어주는지 검증하지 못 합니다. 둘 중 하나로 정리하면 좋겠습니다: 1. **이름과 의도 일치**: 테스트 이름을 `command_status_returns_not_found_for_missing_program` 정도로 바꿔서 "OS 동작 sanity check" 로 명시. helper 내부 동작은 dogfooding 으로 검증한다는 doc comment 추가. 2. **helper 진짜 호출**: `TuiTerminal` 을 mock 하기 어려우면 `cfg_attr(test, allow(...))` 로 internal `_status_only` shim 을 노출해 그것을 테스트. (오버엔지니어링 가능성 있음) 저는 (1) 이 가성비 좋다고 봅니다 — 기존 test 코드는 그대로 두고 이름 + doc 만 손보는 정도.
altair823 added 1 commit 2026-05-03 02:35:14 +00:00
- `App.pending_editor` / `force_redraw` 를 `pub(crate)` 로 좁힘.
  외부 caller (kebab-cli/desktop) 는 enqueue 할 일 없고, 잘못
  mutate 하면 \"set 후 다음 tick 에 drain\" invariant 가 깨짐.
- 외부 read access 가 필요한 경우를 위해 `App::pending_editor()` 읽기
  전용 accessor 추가 — integration test (`tests/search.rs`) 가 사용.
- `App.force_redraw` doc comment 의 \"ratchet incremented\" 표현을
  실제 type (bool flag) 에 맞게 \"when set, the next draw clears\" 로
  교체.
- `editor::tests::unspawnable_program_surfaces_program_name_in_error`
  를 `command_status_returns_not_found_for_missing_program` 으로
  rename + doc 수정 — 실제로 `with_external_program` 호출하지 않고
  `Command::status()` 만 검증한다는 점을 솔직하게 명시 (helper
  end-to-end 는 dogfooding 으로 검증).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-03 02:35:30 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — nit 3 건 모두 깔끔히 반영.

  • pending_editor / force_redraw 가 pub(crate), App::pending_editor() 읽기 전용 accessor 가 외부 read 수요 cover
  • force_redraw doc 의 "ratchet incremented" → "when set, the next draw clears" (실제 bool flag 의미 일치)
  • editor 테스트 이름 + doc 솔직하게 정정 (Command::status sanity, helper end-to-end 는 dogfooding)

추가 지적 없음. 머지 OK.

회차 2 — nit 3 건 모두 깔끔히 반영. - pending_editor / force_redraw 가 pub(crate), App::pending_editor() 읽기 전용 accessor 가 외부 read 수요 cover - force_redraw doc 의 "ratchet incremented" → "when set, the next draw clears" (실제 bool flag 의미 일치) - editor 테스트 이름 + doc 솔직하게 정정 (Command::status sanity, helper end-to-end 는 dogfooding) 추가 지적 없음. 머지 OK.
altair823 merged commit b8b3236899 into main 2026-05-03 02:35:46 +00:00
altair823 deleted branch feat/p9-fb-09-editor 2026-05-03 02:35:47 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#68