From 694eb95c50e75ef3634548c4da8c26954310d524 Mon Sep 17 00:00:00 2001 From: kamiriku Date: Fri, 16 Jan 2026 10:39:36 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A9=B3=E7=B4=B0=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=83=A2=E3=83=BC=E3=83=89=EF=BC=88=E5=85=A8=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=EF=BC=89=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enterキーで詳細表示モードに切り替え、全画面でMarkdownを表示・スクロールできる機能を実装しました。 実装内容: - handleKeyPress() の更新 - 詳細表示モード時の特別なキーバインドを実装 - j/k または 上下矢印: スクロール - g: 先頭へジャンプ - G: 末尾へジャンプ - d: 半ページ下にスクロール - u: 半ページ上にスクロール - q/Esc: リスト選択モードに戻る - Enter キーで詳細表示モードに切り替え - updateDetailView() 関数を実装 - viewportのサイズを全画面用に調整 - Markdownをレンダリング - 先頭にジャンプ - renderDetail() を全画面表示に更新 - ヘッダー表示(Task #X/Y) - viewport で全画面表示 - 区切り線 - 操作ガイド - styles.go の更新 - DetailHeaderStyle を追加(青色背景) 機能: - Enter キーで全画面詳細表示に切り替え - タスクの全文を快適に閲覧 - j/k でスムーズにスクロール - g/G で先頭/末尾に即座にジャンプ - d/u で半ページスクロール - q/Esc でリスト表示に戻る - Markdownの美しいレンダリング 受け入れ基準: ✅ Enter キーで全画面表示に切り替わる ✅ タスクの全文が表示される ✅ j/k でスクロールできる ✅ gg/G で先頭/末尾にジャンプできる ✅ q でリスト選択モードに戻る ✅ 長文タスクが快適に閲覧できる Closes #14 --- internal/ui/models.go | 112 +++++++++++++++++++++++++++++++++++------- internal/ui/styles.go | 7 +++ 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/internal/ui/models.go b/internal/ui/models.go index d0244f6..6b762e5 100644 --- a/internal/ui/models.go +++ b/internal/ui/models.go @@ -82,6 +82,46 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // handleKeyPress はキー入力を処理します func (m model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + // 詳細表示モード時の特別なキーバインド + if m.currentMode == modeDetail { + switch msg.String() { + case "ctrl+c": + m.quitting = true + return m, tea.Quit + case "q", "esc": + // リスト選択モードに戻る + m.currentMode = modeList + m.updatePreview() + return m, nil + case "g": + // 先頭へジャンプ + m.viewport.GotoTop() + return m, nil + case "G": + // 末尾へジャンプ + m.viewport.GotoBottom() + return m, nil + case "j", "down": + // 下にスクロール + m.viewport.LineDown(1) + return m, nil + case "k", "up": + // 上にスクロール + m.viewport.LineUp(1) + return m, nil + case "d": + // 半ページ下にスクロール + m.viewport.HalfViewDown() + return m, nil + case "u": + // 半ページ上にスクロール + m.viewport.HalfViewUp() + return m, nil + } + return m, nil + } + + // リスト選択モード時のキーバインド switch msg.String() { case "ctrl+c", "q": m.quitting = true @@ -98,17 +138,9 @@ func (m model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.updatePreview() // プレビューを更新 } case "enter": - // 詳細表示への切り替え(後で実装) - if m.currentMode == modeList { - m.currentMode = modeDetail - } else { - m.currentMode = modeList - } - case "esc": - // リスト表示に戻る - if m.currentMode == modeDetail { - m.currentMode = modeList - } + // 詳細表示への切り替え + m.currentMode = modeDetail + m.updateDetailView() } return m, nil } @@ -151,6 +183,36 @@ func (m *model) updatePreview() { m.viewport.SetContent(rendered) } +// updateDetailView は詳細表示モードのビューを更新します +func (m *model) updateDetailView() { + if !m.ready || len(m.pending) == 0 || m.cursor >= len(m.pending) { + m.viewport.SetContent("タスクが見つかりません") + return + } + + task := m.pending[m.cursor] + + // 詳細表示用のviewportサイズを調整(全画面に近い) + detailHeight := m.height - 6 // ヘッダーとヘルプを考慮 + if detailHeight < 10 { + detailHeight = 10 + } + m.viewport.Width = m.width - 4 + m.viewport.Height = detailHeight + + // Markdownをレンダリング(全画面用) + rendered, err := render.RenderMarkdown(task.Text, m.width-4) + if err != nil { + // エラー時は生テキストを表示 + m.viewport.SetContent(task.Text) + } else { + m.viewport.SetContent(rendered) + } + + // 先頭にジャンプ + m.viewport.GotoTop() +} + // renderList はリスト表示を描画します(プレビュー付き2ペイン表示) func (m model) renderList() string { var s strings.Builder @@ -237,17 +299,33 @@ func (m model) renderList() string { return s.String() } -// renderDetail は詳細表示を描画します(骨組み) +// renderDetail は詳細表示を描画します(全画面表示) func (m model) renderDetail() string { if m.cursor >= len(m.pending) { return "タスクが見つかりません\n" } - task := m.pending[m.cursor] + var s strings.Builder + + // ヘッダー(タスク番号と情報) + taskNum := fmt.Sprintf("Task #%d/%d", m.cursor+1, len(m.pending)) + s.WriteString(DetailHeaderStyle.Render(taskNum) + "\n\n") + + // viewport で全画面表示 + if m.ready { + s.WriteString(m.viewport.View() + "\n") + } - s := HeaderStyle.Render("📄 Task Detail") + "\n\n" - s += DetailStyle.Render(task.Text) + "\n\n" - s += HelpStyle.Render("Esc/Enter: 戻る | q: 終了") + "\n" + // 区切り線 + dividerWidth := m.width - 2 + if dividerWidth < 10 { + dividerWidth = 10 + } + divider := strings.Repeat("─", dividerWidth) + s.WriteString("\n" + DividerStyle.Render(divider) + "\n") - return s + // ヘルプ + s.WriteString(HelpStyle.Render("j/k: スクロール | g/G: 先頭/末尾 | d/u: 半ページ | q/Esc: 戻る") + "\n") + + return s.String() } diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 2818ea3..2905583 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -42,4 +42,11 @@ var ( Foreground(lipgloss.Color("15")). Background(lipgloss.Color("99")). Padding(0, 1) + + // DetailHeaderStyle は詳細表示ヘッダー用のスタイル + DetailHeaderStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("15")). + Background(lipgloss.Color("33")). + Padding(0, 1) )