Skip to content

Navigation Menu

Sign in
Sign up

fix: g (go to top) was advertised in help but never handled - #152

Open
piotrsynowiec wants to merge 1 commit into
gammons:main from
piotrsynowiec:fix/gg-top-never-wired
Open

fix: g (go to top) was advertised in help but never handled #152
piotrsynowiec wants to merge 1 commit into
gammons:main from
piotrsynowiec:fix/gg-top-never-wired

Conversation

@piotrsynowiec

@piotrsynowiec piotrsynowiec commented Aug 18, 2026

Copy link
Copy Markdown

What breaks

Press ?. The help modal lists gg — top. Press g. Nothing happens.

G jumps to the bottom in every panel. Its counterpart does nothing, anywhere: sidebar, messages pane, thread panel, threads list.

Why

keys.Top is fully declared:

Top: key.NewBinding(key.WithKeys("g"), key.WithHelp("gg", "top")),
Bottom: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "bottom")),

help.FromKeyMap builds the help modal by reflecting over every binding that carries help text, so Top has always been shown to users — a binding does not have to be wired to be advertised.

But handleNormalMode has a case for keys.Bottom and none for keys.Top, and no handleGoToTop exists. The binding is declared, documented, and unreachable.

Worth noting: GoToTop is already implemented on sidebar, messages, threadsview and thread — four models, none of them called from anywhere. That is what makes this look like intended-then-forgotten rather than a deliberate omission; nothing else explains four unused implementations sitting behind a documented shortcut.

The fix

Add handleGoToTop, mirroring handleGoToBottom, and dispatch keys.Top in handleNormalMode.

One addition over a pure mirror: arriving at the top of the channel history is slk's history-backfill trigger, so the messages branch fires maybeFetchOlderHistory the same way the wheel and k paths already do. Without that, g would land at the top and not load the older messages every other route to that position loads.

Testing

Three tests: g moves the sidebar selection to the first row, g moves the messages pane to the top, and a guard that the help modal genuinely lists "top".

That third one is the point of the pair. A binding nothing dispatches on is only a bug because users are told it exists — if the help entry is ever removed, the test says so instead of quietly passing.

Verified by reverting the fix and confirming the tests fail on the defect.

Full suite passes.

How I found it

While auditing a related class of defect: decisions slk makes in more than one place — keyboard vs mouse, per-view routing — where one copy learned about a new case and the other did not. This one is the degenerate version: the decision has one copy, and it was never written.


🤖 Generated with Claude Code

keys.Top is declared, bound to "g", and carries help text ("gg",
"top"). help.FromKeyMap builds the help modal by reflecting over every
binding that has help text, so ? has always listed the shortcut. But
handleNormalMode had no case for keys.Top and no handleGoToTop existed,
so pressing g did nothing -- in the sidebar, the messages pane, the
thread panel and the threads list alike. G worked; its counterpart
silently did not.
Every panel model already had GoToTop. Only the App-level handler and
its dispatch case were missing, which reads as intended-then-forgotten
rather than a deliberate omission -- nothing else would explain four
unused GoToTop implementations and a documented binding.
handleGoToTop mirrors handleGoToBottom, with one addition: reaching the
top of the channel history is slk's backfill trigger, so the messages
branch fires maybeFetchOlderHistory the way the wheel and k paths do.
Tests cover the sidebar and the messages pane, plus a guard that the
help modal really does advertise "top" -- which is what makes the
missing handler a user-visible defect rather than dead configuration.
Verified by reverting the fix and confirming both fail on it.

rfist commented Aug 21, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Thanks, @piotrsynowiec. I checked this independently and ran into the same issue before finding this PR. It looks like keys.Top is declared, help.FromKeyMap includes it because it has help text, but handleNormalMode doesn't handle it. All four models already have a working GoToTop, but nothing currently calls it.

I only have one suggestion: the binding.

Right now, this makes g a command, so pressing it once jumps to the top. But the help text says gg, and that's also how vim handles it: g is a prefix, not a command by itself. Both versions are usable, and with the current binding, typing gg just jumps twice, so this isn't really a bug. Still, using g on its own means it can't be used as a prefix later:

  • Any future g command becomes impossible, since the first g has already acted. The ones that would actually fit a messenger probably aren't the text-editing motions but things like gt / gT for next / previous channel (vim's tab bindings, which slk has no equivalent for today outside the finder), gx to open the link in the selected message, or gf for a file attachment.
  • The help says gg, but the actual binding is just g, which is a little confusing.

The chord version doesn't add much code, and the project already has a similar pattern with pendingWinCmd for the ctrl+w prefix. The three main changes would be:

// app.go — alongside pendingWinCmd
pendingGoTop bool
// mode_normal.go — intercepted with the other pending chords
if a.pendingGoTop {
 a.pendingGoTop = false
 a.statusbar.SetHelpHint(a.defaultHelpHint())
 return a.handleGoTopChord(msg)
}
// mode_normal.go — arm it
case key.Matches(msg, a.keys.Top):
 a.pendingGoTop = true
 a.statusbar.SetHelpHint("g ...")
 return nil

Then we'd add a small handleGoTopChord that only acts when the second key is g and quietly ignores anything else, just like handleWindowChord. SetMode should also clear pendingGoTop, just as it clears pendingWinCmd; otherwise, since ctrl+c is handled before mode dispatch, the prefix could remain active.

Ignoring the second key is important here: in vim, g is a prefix, so gj shouldn't fall through and move down.

Either approach is fine—the bug is real, and this PR fixes it, which is the important part. I just think it's worth deciding now whether g should stay available for future commands. I have the chord version written and tested locally if it's helpful; I can open it as a follow-up on top of this rather than make it compete with the current PR.

gammons commented Sep 3, 2026

Copy link
Copy Markdown
Owner

The bug is real: internal/ui/keys.go:89 declares Top with help text gg, there's no keys.Top case anywhere in mode_normal.go, and help.FromKeyMap shows it regardless. Four unused GoToTop implementations exist (sidebar/model.go:846, messages/model.go:965, threadsview/model.go:305, thread/model.go:769). Intended, then forgotten. Good find, and g/G is table stakes for a vim-modal TUI.

But the one piece of logic that isn't a copy of handleGoToBottom is dead code. I measured it on your branch:

before g: yOffset=586 AtTop=false ViewportAtTop=false
after g: yOffset=586 AtTop=true ViewportAtTop=false cmd_nil=true
RESULT: g returned nil cmd -> backfill did NOT fire

handleGoToTop returns a.maybeFetchOlderHistory(a.messagepane.ViewportAtTop()), but ViewportAtTop() reads m.yOffset (messages/model.go:987), which GoToTop() never touches — it only sets selected = 0 (messages/model.go:965-970). yOffset is recomputed lazily during render, i.e. after the cmd is returned. So from any scrolled-down position — the only position where g is useful — the backfill never fires.

The right predicate is the selection-based AtTop(), which is what the k path uses at app.go:1328. Your comment says you're firing "the same one the wheel and k paths fire," but you copied the wheel predicate (app.go:1454, reducer_mouse.go:122) onto a selection move. One identifier:

after g: AtTop=true ViewportAtTop=false cmd_nil=false
RESULT: backfill cmd returned

The tests never touch the broken part. TestGoToTopScrollsChannelHistory asserts a.messagepane.AtTop() while the handler branches on ViewportAtTop() — two different predicates, and asserting the one the code doesn't use is exactly why this shipped green. It's also near-tautological (GoToTop sets selected = 0; AtTop() is selected == 0). TestTopKeyIsAdvertisedInHelp passes on unmodified main, so it guards the help entry, not the fix. Nothing asserts the backfill cmd at all.

Also please drop the two t.Skip guards in gototop_test.go:47 and :76 — a test that silently skips when the fixture doesn't cooperate is a test that can silently stop testing.

On g vs gg@rfist is right, and this is the bigger issue. The entire justification is "help says gg, pressing g does nothing," and the fix makes g an immediate command while leaving help saying gg and the binding as g. The stated defect is only half-resolved.

We already have the mechanism: pendingWinCmd (app.go:143-146, mode_normal.go:42-46, armed at :92-95, cleared in SetMode at app.go:1613). A pendingGoTop chord is ~15 lines against an established in-repo pattern, and it keeps g free as a prefix for gt/gT/gx/gf — all of which suit a messenger better than vim's text motions. Once g ships as an immediate command, reclaiming it is a breaking change.

Either implement the chord (@rfist has it written) or change keys.go:89 to key.WithHelp("g", "top"). Not both ways.

No conflict with #163 or #164 — both touch the same switch and keys.go block but neither binds g. Trivial rebase.

Your CI lint failure is not your fault — old golangci-lint panicking under go1.27, fixed on main by 6d39fe5. Your test job passed.

@gammons gammons added the changes requested Blocking issues found in review label Sep 3, 2026
@gammons gammons reopened this Sep 3, 2026

gammons commented Sep 3, 2026

Copy link
Copy Markdown
Owner

#171 has landed and I've re-run CI here — the lint failure is gone and this is green now. That was the stale golangci-lint/go1.27 issue, not anything you did.

Note that #171 also enabled gofmt as an enforced lint check, so please run gofmt -w over your changes when you push the next revision.

My review above still stands — that's what's needed to move this forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

changes requested Blocking issues found in review

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /