Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/tui/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.cursor++
}
m.adjustOffset()
return m, nil
return m, m.maybeFetchMore()
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
Expand Down Expand Up @@ -52,7 +52,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.cursor++
}
m.adjustOffset()
return m, nil
return m, m.maybeFetchMore()
case "k":
if m.cursor > 0 {
m.cursor--
Expand All @@ -68,7 +68,7 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.cursor = listLen - 1
}
m.adjustOffset()
return m, nil
return m, m.maybeFetchMore()
case "l":
return m.navigateForward()
case "h":
Expand Down
105 changes: 91 additions & 14 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,29 @@ const (
modeTimeInput
)

// logGroupsMsg is sent when log groups are fetched.
type logGroupsMsg []aws.LogGroup
// logGroupsPageMsg is sent when the first page of log groups is fetched.
type logGroupsPageMsg struct {
groups []aws.LogGroup
nextToken *string
}

// logStreamsPageMsg is sent when the first page of log streams is fetched.
type logStreamsPageMsg struct {
streams []aws.LogStream
nextToken *string
}

// moreGroupsPageMsg is sent when additional pages of log groups are fetched.
type moreGroupsPageMsg struct {
groups []aws.LogGroup
nextToken *string
}

// logStreamsMsg is sent when log streams are fetched.
type logStreamsMsg []aws.LogStream
// moreStreamsPageMsg is sent when additional pages of log streams are fetched.
type moreStreamsPageMsg struct {
streams []aws.LogStream
nextToken *string
}

// logEventsMsg is sent when log events are fetched.
type logEventsMsg []aws.LogEvent
Expand Down Expand Up @@ -67,8 +85,9 @@ type Model struct {
groupsNextToken *string
streamsNextToken *string

loading bool
err error
loading bool
loadingMore bool
err error

width int
height int
Expand Down Expand Up @@ -132,18 +151,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
return m, nil

case logGroupsMsg:
m.logGroups = []aws.LogGroup(msg)
case logGroupsPageMsg:
m.logGroups = msg.groups
m.groupsNextToken = msg.nextToken
m.loading = false
return m, nil

case logStreamsMsg:
m.logStreams = []aws.LogStream(msg)
case logStreamsPageMsg:
m.logStreams = msg.streams
m.streamsNextToken = msg.nextToken
m.loading = false
m.cursor = 0
m.offset = 0
return m, nil

case moreGroupsPageMsg:
m.logGroups = append(m.logGroups, msg.groups...)
m.groupsNextToken = msg.nextToken
m.loadingMore = false
return m, nil

case moreStreamsPageMsg:
m.logStreams = append(m.logStreams, msg.streams...)
m.streamsNextToken = msg.nextToken
m.loadingMore = false
return m, nil

case logEventsMsg:
m.loading = false
return m, m.openEditor([]aws.LogEvent(msg))
Expand Down Expand Up @@ -175,21 +208,44 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

func (m Model) fetchLogGroups() tea.Cmd {
return func() tea.Msg {
groups, err := m.client.ListLogGroups(m.ctx)
groups, nextToken, err := m.client.ListLogGroupsPage(m.ctx, nil)
if err != nil {
return errMsg{err}
}
return logGroupsMsg(groups)
return logGroupsPageMsg{groups: groups, nextToken: nextToken}
}
}

func (m Model) fetchMoreGroups() tea.Cmd {
token := m.groupsNextToken
return func() tea.Msg {
groups, nextToken, err := m.client.ListLogGroupsPage(m.ctx, token)
if err != nil {
return errMsg{err}
}
return moreGroupsPageMsg{groups: groups, nextToken: nextToken}
}
}

func (m Model) fetchLogStreams(groupName string) tea.Cmd {
return func() tea.Msg {
streams, err := m.client.ListLogStreams(m.ctx, groupName)
streams, nextToken, err := m.client.ListLogStreamsPage(m.ctx, groupName, nil)
if err != nil {
return errMsg{err}
}
return logStreamsPageMsg{streams: streams, nextToken: nextToken}
}
}

func (m Model) fetchMoreStreams() tea.Cmd {
token := m.streamsNextToken
groupName := m.selectedGroup
return func() tea.Msg {
streams, nextToken, err := m.client.ListLogStreamsPage(m.ctx, groupName, token)
if err != nil {
return errMsg{err}
}
return logStreamsMsg(streams)
return moreStreamsPageMsg{streams: streams, nextToken: nextToken}
}
}

Expand Down Expand Up @@ -254,6 +310,27 @@ func (m Model) hasMoreStreams() bool {
return m.streamsNextToken != nil
}

// maybeFetchMore returns a command to fetch the next page if the cursor
// is at the last item and more pages are available.
func (m *Model) maybeFetchMore() tea.Cmd {
if m.loadingMore {
return nil
}
switch m.currentView {
case viewGroups:
if m.hasMoreGroups() && m.cursor >= len(m.logGroups)-1 {
m.loadingMore = true
return m.fetchMoreGroups()
}
case viewStreams:
if m.hasMoreStreams() && m.cursor >= len(m.logStreams)-1 {
m.loadingMore = true
return m.fetchMoreStreams()
}
}
return nil
}

func (m Model) openEditor(events []aws.LogEvent) tea.Cmd {
content := formatter.Format(events)
path, cleanup, err := editor.WriteTempFile(content)
Expand Down
143 changes: 141 additions & 2 deletions internal/tui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestModel_ReceiveLogGroups(t *testing.T) {
{Name: "/aws/ecs/service-b", RetentionDays: 7, StoredBytes: 2048},
}

model, _ := update(m, logGroupsMsg(groups))
model, _ := update(m, logGroupsPageMsg{groups: groups})

if len(model.logGroups) != 2 {
t.Fatalf("expected 2 log groups, got %d", len(model.logGroups))
Expand Down Expand Up @@ -182,7 +182,7 @@ func TestModel_ReceiveLogStreams(t *testing.T) {
{Name: "stream-002", LastEventTimestamp: time.Now().Add(-time.Minute)},
}

m, _ = update(m, logStreamsMsg(streams))
m, _ = update(m, logStreamsPageMsg{streams: streams})

if len(m.logStreams) != 2 {
t.Fatalf("expected 2 streams, got %d", len(m.logStreams))
Expand Down Expand Up @@ -1102,6 +1102,145 @@ func TestModel_PaneHeight_ExactMatch(t *testing.T) {
}
}

// --- Pagination: fetch all pages ---

func TestModel_FetchLogGroups_StoresNextToken(t *testing.T) {
m := NewModel(nil)
token := "next-page-token"

m, _ = update(m, logGroupsPageMsg{
groups: []aws.LogGroup{{Name: "group-1"}},
nextToken: &token,
})

if len(m.logGroups) != 1 {
t.Fatalf("expected 1 group, got %d", len(m.logGroups))
}
if m.groupsNextToken == nil || *m.groupsNextToken != token {
t.Errorf("expected groupsNextToken=%q, got %v", token, m.groupsNextToken)
}
}

func TestModel_FetchLogGroups_NilTokenWhenNoMore(t *testing.T) {
m := NewModel(nil)

m, _ = update(m, logGroupsPageMsg{
groups: []aws.LogGroup{{Name: "group-1"}},
nextToken: nil,
})

if m.groupsNextToken != nil {
t.Errorf("expected nil groupsNextToken, got %v", m.groupsNextToken)
}
}

func TestModel_FetchLogStreams_StoresNextToken(t *testing.T) {
m := NewModel(nil)
m.currentView = viewStreams
token := "stream-token"

m, _ = update(m, logStreamsPageMsg{
streams: []aws.LogStream{{Name: "s1"}},
nextToken: &token,
})

if len(m.logStreams) != 1 {
t.Fatalf("expected 1 stream, got %d", len(m.logStreams))
}
if m.streamsNextToken == nil || *m.streamsNextToken != token {
t.Errorf("expected streamsNextToken=%q, got %v", token, m.streamsNextToken)
}
}

func TestModel_MoreGroupsPage_AppendsToList(t *testing.T) {
m := NewModel(nil)
m.logGroups = []aws.LogGroup{{Name: "group-1"}}
token := "page3"

m, _ = update(m, moreGroupsPageMsg{
groups: []aws.LogGroup{{Name: "group-2"}, {Name: "group-3"}},
nextToken: &token,
})

if len(m.logGroups) != 3 {
t.Fatalf("expected 3 groups after append, got %d", len(m.logGroups))
}
if m.groupsNextToken == nil || *m.groupsNextToken != token {
t.Errorf("expected groupsNextToken=%q, got %v", token, m.groupsNextToken)
}
}

func TestModel_MoreStreamsPage_AppendsToList(t *testing.T) {
m := NewModel(nil)
m.currentView = viewStreams
m.logStreams = []aws.LogStream{{Name: "s1"}}

m, _ = update(m, moreStreamsPageMsg{
streams: []aws.LogStream{{Name: "s2"}, {Name: "s3"}},
nextToken: nil,
})

if len(m.logStreams) != 3 {
t.Fatalf("expected 3 streams after append, got %d", len(m.logStreams))
}
if m.streamsNextToken != nil {
t.Errorf("expected nil streamsNextToken, got %v", m.streamsNextToken)
}
}

func TestModel_CursorAtBottom_TriggersMoreGroupsFetch(t *testing.T) {
m := NewModel(nil)
m.logGroups = makeGroups(5)
token := "more"
m.groupsNextToken = &token
m.cursor = 3
m.width = 100
m.height = 24

// Move cursor to last item
m, cmd := update(m, keyMsg('G'))
if m.cursor != 4 {
t.Errorf("expected cursor 4, got %d", m.cursor)
}
// Should trigger a fetch for more groups
if cmd == nil {
t.Error("expected command to fetch more groups when cursor at bottom with more pages")
}
}

func TestModel_CursorAtBottom_NoFetchWhenNoToken(t *testing.T) {
m := NewModel(nil)
m.logGroups = makeGroups(5)
m.groupsNextToken = nil
m.width = 100
m.height = 24

m, cmd := update(m, keyMsg('G'))
if cmd != nil {
t.Error("expected no command when no more pages")
}
}

func TestModel_CursorAtBottom_TriggersMoreStreamsFetch(t *testing.T) {
m := NewModel(nil)
m.currentView = viewStreams
m.selectedGroup = "/aws/test"
m.logStreams = makeStreams(5)
token := "more-streams"
m.streamsNextToken = &token
m.cursor = 3
m.width = 100
m.height = 24

m, cmd := update(m, keyMsg('G'))
if m.cursor != 4 {
t.Errorf("expected cursor 4, got %d", m.cursor)
}
if cmd == nil {
t.Error("expected command to fetch more streams when cursor at bottom with more pages")
}
}

func TestModel_PaneHeight_FewItemsBothSides(t *testing.T) {
// When both panes have few items, they must still have the same height.
// This verifies padding survives TrimSuffix (not TrimRight which strips all).
Expand Down
14 changes: 14 additions & 0 deletions todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,17 @@
## ペインサイズ不一致修正 (#3)
- [x] 2026-04-14 ペイン高さ/幅テスト作成(5テスト)
- [x] 2026-04-14 render関数の末尾改行によるHeight超過を修正(strings.TrimRight)

## WSLペインサイズ不一致修正 (#6)
- [x] 2026-04-14 render関数のmaxHeightパディングテスト作成(2テスト)
- [x] 2026-04-14 render関数でmaxHeight行にパディング + lipgloss Height()依存を除去

## WSLレイアウト崩れ根本修正 (#7, #8)
- [x] 2026-04-14 strings.TrimRight→TrimSuffixに変更(パディング全除去を防止)
- [x] 2026-04-14 lipgloss JoinHorizontal/JoinVerticalを廃止、行ごとに直接出力組み立て
- [x] 2026-04-14 最終出力をm.height-1行にハードキャップ

## ページネーション修正 (F9)
- [x] 2026-04-14 fetchLogGroups/fetchLogStreamsをPage API対応に変更(NextToken返却)
- [x] 2026-04-14 カーソル末尾到達時に次ページを非同期追加取得(maybeFetchMore)
- [x] 2026-04-14 追加ページはリストにappend(既存データを保持)