feat(kebab-tui): p9-fb-10 partial — CJK width helpers + render audit #87

Merged
altair823 merged 3 commits from feat/p9-fb-10-cjk into main 2026-05-03 09:04:01 +00:00
Owner

Summary

p9-fb-10 (TUI CJK input + wide-char rendering audit) partial ship.

kebab-tui::input::{display_width, truncate_to_display_width} helper
모듈 신규 — unicode-width 위에서 column-단위 width 계산 (ASCII=1,
Hangul/CJK/fullwidth=2, combining=0) + char-boundary 안전 truncate.

library.rs 의 중복 truncate_to_display_width private fn 제거 — 모든
pane 이 단일 source 호출.

Scope

  • crates/kebab-tui/src/input.rs 신규 (162 lines, helper + 9 unit tests)
  • crates/kebab-tui/src/lib.rs mod input; + pub use 등록
  • crates/kebab-tui/src/library.rs 중복 helper 제거 → crate::input::truncate_to_display_width 호출
  • crates/kebab-tui/tests/library.rs Korean + Japanese fixture render audit (TestBackend 80×20)
  • tasks/p9/p9-fb-10-tui-cjk-input.md status plannedin_progress
  • tasks/HOTFIXES.md InputBuffer struct deferral 사유 + IME composing out-of-scope 기록
  • README + HANDOFF — CJK column-width 정확성 + partial deferral 기록

Deferred

spec 의 InputBuffer struct (cursor 가 column 단위 wide-char width 추적) 는
follow-up — Ask/Search/Editor pane 의 String + cursor 일괄 마이그레이션이
회귀 표면이 커서 helper 만 먼저 머지. 백스페이스는 모든 pane 이 이미
String::pop() 사용 (char-aware) → byte-boundary 안전성은 helper 없이도
확보된 상태였고, 본 PR 의 helper 는 rendering width 만 정정.

crossterm 0.28 이 native IME composing surface 미노출 — finalized jamo /
composed glyph 가 KeyCode::Char(c) 로만 도달. preedit handling out of
scope (spec 도 명시).

Test plan

  • cargo test -p kebab-tui — 25 + 9 + ... full suite green
  • cargo clippy -p kebab-tui --all-targets -- -D warnings clean
  • 신규 render test 가 한글 / 일본어 글자가 ratatui frame 에 살아남음 확인
## Summary p9-fb-10 (TUI CJK input + wide-char rendering audit) **partial** ship. `kebab-tui::input::{display_width, truncate_to_display_width}` helper 모듈 신규 — `unicode-width` 위에서 column-단위 width 계산 (ASCII=1, Hangul/CJK/fullwidth=2, combining=0) + char-boundary 안전 truncate. `library.rs` 의 중복 `truncate_to_display_width` private fn 제거 — 모든 pane 이 단일 source 호출. ## Scope - `crates/kebab-tui/src/input.rs` 신규 (162 lines, helper + 9 unit tests) - `crates/kebab-tui/src/lib.rs` `mod input;` + `pub use` 등록 - `crates/kebab-tui/src/library.rs` 중복 helper 제거 → `crate::input::truncate_to_display_width` 호출 - `crates/kebab-tui/tests/library.rs` Korean + Japanese fixture render audit (TestBackend 80×20) - `tasks/p9/p9-fb-10-tui-cjk-input.md` status `planned` → `in_progress` - `tasks/HOTFIXES.md` InputBuffer struct deferral 사유 + IME composing out-of-scope 기록 - README + HANDOFF — CJK column-width 정확성 + partial deferral 기록 ## Deferred spec 의 `InputBuffer` struct (cursor 가 column 단위 wide-char width 추적) 는 follow-up — Ask/Search/Editor pane 의 String + cursor 일괄 마이그레이션이 회귀 표면이 커서 helper 만 먼저 머지. 백스페이스는 모든 pane 이 이미 `String::pop()` 사용 (char-aware) → byte-boundary 안전성은 helper 없이도 확보된 상태였고, 본 PR 의 helper 는 **rendering width** 만 정정. crossterm 0.28 이 native IME composing surface 미노출 — finalized jamo / composed glyph 가 `KeyCode::Char(c)` 로만 도달. preedit handling out of scope (spec 도 명시). ## Test plan - `cargo test -p kebab-tui` — 25 + 9 + ... full suite green - `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean - 신규 render test 가 한글 / 일본어 글자가 ratatui frame 에 살아남음 확인
altair823 added 1 commit 2026-05-03 08:53:35 +00:00
`kebab-tui::input::{display_width, truncate_to_display_width}` 신규.
unicode-width 위에서 column-단위 width 계산 (ASCII=1, Hangul/CJK/
fullwidth=2, combining=0) + char-boundary 안전 truncate.

library.rs 의 중복 `truncate_to_display_width` private fn 제거 — 단일
source 로 통일. 9 unit tests + 1 integration render test (Korean +
Japanese fixture, TestBackend 80×20).

spec 의 `InputBuffer` struct 도입은 follow-up — Ask/Search/Editor pane
의 String + cursor 일괄 마이그레이션이 회귀 표면이 커서 helper 만
먼저 머지. backspace 는 모든 pane 이 이미 `String::pop()` 사용 → byte-
boundary 안전성 확보. crossterm 0.28 가 native IME composing 미노출 →
preedit handling out of scope.

spec status `planned` → `in_progress`. HOTFIXES.md 에 InputBuffer
struct deferral 사유 기록.
claude-reviewer-01 requested changes 2026-05-03 08:54:58 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — 핵심 actionable 1건 + nit 2건.

핵심: library.rs format_doc_row{title:<title_w$} padding 이 std::fmt 의 char-count 기반이라 wide char title 에서 column drift. 본 PR 이 truncate 는 정확히 잡지만 padding 은 안 잡아 — 같은 PR 에 같이 가는 게 일관 (CJK column 정확성 = 본 PR 의 stated goal).

nit: input.rs 테스트 코멘트의 = wait 잔재 + HOTFIXES 의 후속 체크리스트가 owner 없이 떠 있음.

회차 1 — 핵심 actionable 1건 + nit 2건. **핵심**: library.rs `format_doc_row` 의 `{title:<title_w$}` padding 이 std::fmt 의 char-count 기반이라 wide char title 에서 column drift. 본 PR 이 truncate 는 정확히 잡지만 padding 은 안 잡아 — 같은 PR 에 같이 가는 게 일관 (CJK column 정확성 = 본 PR 의 stated goal). **nit**: input.rs 테스트 코멘트의 `= wait` 잔재 + HOTFIXES 의 후속 체크리스트가 owner 없이 떠 있음.
@@ -0,0 +97,4 @@
/// p9-fb-10: mixed ASCII + Hangul sums correctly.
#[test]
fn mixed_ascii_hangul_width() {
// "kb-한글" = 2 ASCII + 1 dash + 2 Hangul × 2 = 5 + 4 = wait

nit (cosmetic): 테스트 코멘트에 디버깅 흔적 남음 — // "kb-한글" = 2 ASCII + 1 dash + 2 Hangul × 2 = 5 + 4 = wait= wait 가 thinking-out-loud 잔재. 의도는 다음 줄의 정정 ("k" 1 + "b" 1 + ... = 7) 이지만 코멘트 한 줄로 충분: // "kb-한글" = k(1) + b(1) + -(1) + 한(2) + 글(2) = 7.

**nit (cosmetic)**: 테스트 코멘트에 디버깅 흔적 남음 — `// "kb-한글" = 2 ASCII + 1 dash + 2 Hangul × 2 = 5 + 4 = wait` 의 `= wait` 가 thinking-out-loud 잔재. 의도는 다음 줄의 정정 (`"k" 1 + "b" 1 + ...` = 7) 이지만 코멘트 한 줄로 충분: `// "kb-한글" = k(1) + b(1) + -(1) + 한(2) + 글(2) = 7`.

Padding bug — pre-existing, surfaced by 본 PR 의 CJK 머지: format!("{title:<title_w$}", ...) 는 std::fmt 의 width 가 char count 기반이라 wide char 가 들어가면 padding 이 모자라. 예: title=러스트로 만드는 지식 베이스(13 chars / 25 cols), title_w=30 → fmt 는 30-13=17 spaces 추가 → 총 13+17=30 chars 지만 display 는 25+17=42 cols 가 되어 columns 가 밀림. 본 PR 의 truncate_to_display_width 로 title.width() <= title_w 는 보장되지만 padding 후의 over-width 는 별개. 수정안: pad 를 명시적으로 계산 — let pad = title_w.saturating_sub(crate::input::display_width(&title)); let title = format!("{title}{:width$}", "", width = pad); 후 format string 에서 {title} 만 (no <title_w$>). 같은 pane 의 tags:<12 / updated_short:<10 도 동일 — tags 에 한글이 들어가면 (이번 PR test fixture 한글 tag) column drift. 본 PR scope 가 helper + 단일 source 통합이므로 padding 수정은 follow-up commit (또는 별 PR) 으로 splitting 할 만하지만, 기왕 CJK 머지하는 김에 같은 PR 에 끼우는 게 사용자 입장 일관.

**Padding bug — pre-existing, surfaced by 본 PR 의 CJK 머지**: `format!("{title:<title_w$}", ...)` 는 std::fmt 의 width 가 **char count** 기반이라 wide char 가 들어가면 padding 이 모자라. 예: title=`러스트로 만드는 지식 베이스`(13 chars / 25 cols), title_w=30 → fmt 는 30-13=17 spaces 추가 → 총 13+17=30 chars 지만 display 는 25+17=42 cols 가 되어 columns 가 밀림. 본 PR 의 truncate_to_display_width 로 title.width() <= title_w 는 보장되지만 padding 후의 over-width 는 별개. 수정안: pad 를 명시적으로 계산 — `let pad = title_w.saturating_sub(crate::input::display_width(&title)); let title = format!("{title}{:width$}", "", width = pad);` 후 format string 에서 `{title}` 만 (no `<title_w$>`). 같은 pane 의 `tags:<12` / `updated_short:<10` 도 동일 — tags 에 한글이 들어가면 (이번 PR test fixture `한글` tag) column drift. 본 PR scope 가 helper + 단일 source 통합이므로 padding 수정은 follow-up commit (또는 별 PR) 으로 splitting 할 만하지만, 기왕 CJK 머지하는 김에 같은 PR 에 끼우는 게 사용자 입장 일관.
@@ -17,0 +38,4 @@
handling 은 out-of-scope (spec 도 "not in scope" 로 명시).
**후속 spec issue**: InputBuffer 도입 시 (a) 모든 pane 의 input string
을 InputBuffer 로 교체, (b) cursor render 가 wide-char 위에서 column

nit: **후속 spec issue**: ... 절이 actionable owner 없이 떠 있음. (a)/(b)/(c) 가 InputBuffer follow-up PR 에 들어갈 항목이라는 건 명확하지만, 어디에 적힐지 (별 spec issue 파일? 본 spec 의 DoD 갱신?) 가 모호. 제안: tasks/p9/p9-fb-10-tui-cjk-input.md## Notes 절 안에 ### Follow-up checklist 로 옮기거나, 같은 HOTFIXES 항목 내에 **후속 PR 체크리스트**: 로 명시. 현재 상태로는 사람이 spec 만 봐도 다음 작업 파악 가능해야 한다는 frozen-spec 원칙에 살짝 어긋남.

**nit**: `**후속 spec issue**: ...` 절이 actionable owner 없이 떠 있음. (a)/(b)/(c) 가 InputBuffer follow-up PR 에 들어갈 항목이라는 건 명확하지만, 어디에 적힐지 (별 spec issue 파일? 본 spec 의 DoD 갱신?) 가 모호. 제안: `tasks/p9/p9-fb-10-tui-cjk-input.md` 의 `## Notes` 절 안에 `### Follow-up checklist` 로 옮기거나, 같은 HOTFIXES 항목 내에 `**후속 PR 체크리스트**:` 로 명시. 현재 상태로는 사람이 spec 만 봐도 다음 작업 파악 가능해야 한다는 frozen-spec 원칙에 살짝 어긋남.
altair823 added 1 commit 2026-05-03 08:57:06 +00:00
- format_doc_row: title/tags padding 을 display_width 기반으로 명시
  계산 (std::fmt 의 char-count 기반 `<width$>` 가 wide char 에서 column
  drift). truncate 가 보장하는 width 계약 위에 padding 도 같은 단위로
  통일.
- input.rs 테스트 코멘트 cleanup (`= wait` 디버깅 잔재 제거).
- HOTFIXES "후속 spec issue" → "후속 PR 체크리스트" 로 owner 명시,
  체크박스 5 개로 actionable 화.
claude-reviewer-01 requested changes 2026-05-03 08:57:44 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 actionable 모두 반영됨 (format_doc_row padding 정확화 + 코멘트 cleanup + HOTFIXES checklist owner 명시). 신규 nit 2 건만 — regression hardening + maintainability.

본 회차 actionable: 직접 unit test (format_doc_row 의 column drift 회귀 catch) + tags column width const 추출.

회차 2 — 회차 1 actionable 모두 반영됨 (format_doc_row padding 정확화 + 코멘트 cleanup + HOTFIXES checklist owner 명시). 신규 nit 2 건만 — regression hardening + maintainability. 본 회차 actionable: 직접 unit test (format_doc_row 의 column drift 회귀 catch) + tags column width const 추출.
@@ -193,43 +193,33 @@ pub(crate) fn format_doc_row(d: &DocSummary, title_w: usize) -> String {
} else {
d.tags.join(",")
};
let tags = truncate_to_display_width(&tags, 12);

nit: tags column width 12 가 두 군데 반복 (line 196 truncate_to_display_width 인자 + line 209 12usize.saturating_sub). 한 곳에서 바꾸고 다른 곳을 잊으면 column drift 재발. const TAGS_COL_W: usize = 12; 으로 빼면 maintainability ↑. title_w 는 area.width 에서 동적 계산되니 그대로 두고, tags 만.

**nit**: tags column width `12` 가 두 군데 반복 (line 196 truncate_to_display_width 인자 + line 209 `12usize.saturating_sub`). 한 곳에서 바꾸고 다른 곳을 잊으면 column drift 재발. `const TAGS_COL_W: usize = 12;` 으로 빼면 maintainability ↑. title_w 는 area.width 에서 동적 계산되니 그대로 두고, tags 만.
@@ -205,0 +206,4 @@
// from `display_width`, then concatenate — the truncate above
// already guarantees `display_width(title) <= title_w`.
let title_pad = title_w.saturating_sub(display_width(&title));
let tags_pad = 12usize.saturating_sub(display_width(&tags));

nit (regression hardening): format_doc_row 의 padding 수정 자체에 대한 직접 unit test 가 없음 — render 테스트 (library_renders_korean_titles_without_overflow) 가 글자 presence 만 확인하지 column alignment 는 안 확인. 즉 누군가 <title_w$> 로 되돌려도 render test 는 통과. 제안: format_doc_row(&doc_with_hangul_title, 20) 직접 호출 → display_width(&row) == expected_total_cols assert. 그러면 한글 padding 회귀가 unit-level 에서 잡힘. 본 PR 의 tests/library.rs 끝에 추가하거나 새 format_doc_row_* 그룹으로 묶기.

**nit (regression hardening)**: format_doc_row 의 padding 수정 자체에 대한 **직접 unit test** 가 없음 — render 테스트 (`library_renders_korean_titles_without_overflow`) 가 글자 presence 만 확인하지 column alignment 는 안 확인. 즉 누군가 `<title_w$>` 로 되돌려도 render test 는 통과. 제안: `format_doc_row(&doc_with_hangul_title, 20)` 직접 호출 → `display_width(&row) == expected_total_cols` assert. 그러면 한글 padding 회귀가 unit-level 에서 잡힘. 본 PR 의 `tests/library.rs` 끝에 추가하거나 새 `format_doc_row_*` 그룹으로 묶기.
altair823 added 1 commit 2026-05-03 08:59:29 +00:00
- TAGS_COL_W const 추출 (truncate + pad 동시 사용 — drift 방지).
- format_doc_row 직접 unit test 2개 (Hangul title / Hangul tag) 가
  display column 정렬을 정확히 pin. `<title_w$>` 으로 되돌리는
  회귀가 unit-level 에서 catch 됨.
claude-reviewer-01 approved these changes 2026-05-03 08:59:59 +00:00
claude-reviewer-01 left a comment
Member

회차 3 — 회차 2 actionable 모두 깔끔히 반영됨.

  • TAGS_COL_W const 추출로 truncate / pad drift 가능성 제거.
  • format_doc_row 직접 unit test 2 건 (Hangul title 30 cols → 59, Hangul tag 20 cols → 49) 가 column drift 회귀를 unit-level 에서 catch. <title_w$> 으로 되돌리는 변경이 즉시 빨간 줄.

본 PR scope (CJK helper + library padding + helper 단일 source 통합 + render audit) 모두 일관되게 closed. spec 의 InputBuffer struct 도입은 HOTFIXES 의 5-항목 체크리스트로 owner 명시 — 다음 PR 가 그대로 picking up 가능. 머지 OK.

회차 3 — 회차 2 actionable 모두 깔끔히 반영됨. - `TAGS_COL_W` const 추출로 truncate / pad drift 가능성 제거. - `format_doc_row` 직접 unit test 2 건 (Hangul title 30 cols → 59, Hangul tag 20 cols → 49) 가 column drift 회귀를 unit-level 에서 catch. `<title_w$>` 으로 되돌리는 변경이 즉시 빨간 줄. 본 PR scope (CJK helper + library padding + helper 단일 source 통합 + render audit) 모두 일관되게 closed. spec 의 InputBuffer struct 도입은 HOTFIXES 의 5-항목 체크리스트로 owner 명시 — 다음 PR 가 그대로 picking up 가능. 머지 OK.
altair823 merged commit bbe83ca00b into main 2026-05-03 09:04:01 +00:00
altair823 deleted branch feat/p9-fb-10-cjk 2026-05-03 09:04:21 +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#87