Quellcode durchsuchen

feat(ui/chat): add configurable arrow key navigation

Arrow up/down do the same as k/j in both panels, left/right cycle
between guilds and messages endlessly. All four are separate keybinds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
claude vor 1 Monat
Ursprung
Commit
c6c87b5549

+ 3 - 1
CLAUDE.md

@@ -49,7 +49,7 @@ This is the persistent context file for Claude Code. Keep it concise and useful.
 - **Attachments**: URL fix (proper `NewLine()` + `.Url()` style), `o` in ShortHelp when attachments/URLs present
 - **Help/Config**: `?` keybind (was `ctrl+.`), `E` edits config in `$EDITOR`/vim from help overlay
 - **State persistence**: guild + channel expand/collapse state in `~/.cache/discordo/state.json` (`guildstate.go`)
-- **Focus/Navigation**: AutoFocus targets messages list on channel select; ESC cycles input→messages→guilds→input; left/right arrows between panels
+- **Focus/Navigation**: AutoFocus targets messages list on channel select; ESC cycles input→messages→guilds→input; arrow keys: up/down same as k/j, left/right cycle between panels (configurable keybinds)
 - **Security hardening**: path traversal prevention, HTTPS-only downloads, bounded downloads (100MB), `exec.LookPath` validation, atomic writes, restrictive perms, direct `exec.Command` (no `sh -c`)
 - **Bug fixes**: Brotli body leak, cache type assertion panic, MarkRead uses newest fetched message ID
 - **Code structure**: extracted `url_extractor.go`, `embed_renderer.go`, `attachment_handler.go` from `messages_list.go`; replaced `open-golang` with stdlib
@@ -85,10 +85,12 @@ To add a new site-specific URL label, edit `ui.LinkDisplayText()` in `internal/u
 - `keybinds.messages_list.open_thread` — navigate to message's thread (default: `t`)
 - `keybinds.messages_list.user_info` — show author info popup (default: `w`)
 - `keybinds.command_mode` — open vim-style command input (default: `:`)
+- `keybinds.guilds_tree.arrow_*` / `keybinds.messages_list.arrow_*` — arrow key navigation (default: `up`/`down`/`left`/`right`)
 
 ## Build & Run
 - Build: `go build -o discordo-plus .`
 - Install: `sudo mv discordo-plus /usr/local/bin/`
+- Arch Linux: `makepkg -si` (uses `PKGBUILD`, installs via pacman)
 - Run: `discordo-plus`
 - Test: `go test ./...` (only config + keyring packages have tests)
 - Dependencies: `mpv` (or configured image viewer), `xdotool` (optional, X11 geometry detection), `xdg-open` (Linux, for default opener)

+ 7 - 1
README.md

@@ -10,7 +10,8 @@ A fork of [discordo](https://github.com/ayn2op/discordo) — a lightweight Disco
 - **Link display compression**: URLs are shown as human-friendly labels — Discord CDN links show the filename, known sites show short labels (e.g., "Tenor GIF", "YouTube", "GitHub - owner/repo"), others show host + truncated path. Garbled CDN filenames (URL-encoded URLs, UUIDs) are cleaned up. Redundant link preview embeds are suppressed. Full URLs remain clickable via OSC 8 hyperlinks.
 
 ### Navigation & UI
-- **ESC focus cycling**: ESC cycles focus between input → messages → guilds → input; arrow keys navigate between panels
+- **Arrow key navigation**: Up/down arrows move within a panel (same as k/j), left/right arrows cycle between guilds and messages panels. All four are separate configurable keybinds.
+- **ESC focus cycling**: ESC cycles focus between input → messages → guilds → input
 - **Search**: `/` opens fuzzy search over current channel messages
 - **Threads**: Thread indicator on messages, `t` navigates into thread
 - **Reactions**: Displayed below messages, bold for own reactions, real-time gateway updates
@@ -44,6 +45,11 @@ sudo pacman -S go mpv xdotool wl-clipboard
 ```bash
 git clone https://gogs.altsol.dev/claude/discordo-plus.git
 cd discordo-plus
+
+# Option 1: Install via pacman (Arch Linux)
+makepkg -si
+
+# Option 2: Manual install
 go build -o discordo-plus .
 sudo install -Dm755 discordo-plus /usr/local/bin/discordo-plus
 ```

+ 8 - 0
internal/config/config.toml

@@ -138,6 +138,10 @@ select_current = "enter"
 yank_id = "i"
 collapse_parent_node = "-"
 move_to_parent_node = "p"
+arrow_up = "up"
+arrow_down = "down"
+arrow_left = "left"
+arrow_right = "right"
 
 # Only while focusing on sent messages
 [keybinds.messages_list]
@@ -168,6 +172,10 @@ save_image = "S"
 user_info = "w"
 open_thread = "t"
 search = "/"
+arrow_up = "up"
+arrow_down = "down"
+arrow_left = "left"
+arrow_right = "right"
 # Yank (copy) the selected message's content/url/id.
 yank_content = "y"
 yank_url = "u"

+ 18 - 0
internal/config/keybinds.go

@@ -74,6 +74,11 @@ type GuildsTreeKeybinds struct {
 
 	CollapseParentNode Keybind `toml:"collapse_parent_node"`
 	MoveToParentNode   Keybind `toml:"move_to_parent_node"`
+
+	ArrowUp    Keybind `toml:"arrow_up"`
+	ArrowDown  Keybind `toml:"arrow_down"`
+	ArrowLeft  Keybind `toml:"arrow_left"`
+	ArrowRight Keybind `toml:"arrow_right"`
 }
 
 type MessagesListKeybinds struct {
@@ -95,6 +100,11 @@ type MessagesListKeybinds struct {
 	Search     Keybind `toml:"search"`
 	OpenThread Keybind `toml:"open_thread"`
 
+	ArrowUp    Keybind `toml:"arrow_up"`
+	ArrowDown  Keybind `toml:"arrow_down"`
+	ArrowLeft  Keybind `toml:"arrow_left"`
+	ArrowRight Keybind `toml:"arrow_right"`
+
 	YankContent Keybind `toml:"yank_content"`
 	YankURL     Keybind `toml:"yank_url"`
 	YankID      Keybind `toml:"yank_id"`
@@ -169,6 +179,10 @@ func defaultGuildsTreeKeybinds() GuildsTreeKeybinds {
 		YankID:             newKeybind("i", "copy id"),
 		CollapseParentNode: newKeybind("-", "collapse"),
 		MoveToParentNode:   newKeybind("p", "parent"),
+		ArrowUp:            newKeybind("up", "up"),
+		ArrowDown:          newKeybind("down", "down"),
+		ArrowLeft:          newKeybind("left", "left"),
+		ArrowRight:         newKeybind("right", "right"),
 	}
 }
 
@@ -201,6 +215,10 @@ func defaultMessagesListKeybinds() MessagesListKeybinds {
 		UserInfo:    newKeybind("w", "who"),
 		Search:      newKeybind("/", "search"),
 		OpenThread:  newKeybind("t", "thread"),
+		ArrowUp:     newKeybind("up", "up"),
+		ArrowDown:   newKeybind("down", "down"),
+		ArrowLeft:   newKeybind("left", "left"),
+		ArrowRight:  newKeybind("right", "right"),
 		YankContent: newKeybind("y", "copy text"),
 		YankURL:     newKeybind("u", "copy url"),
 		YankID:      newKeybind("i", "copy id"),

+ 6 - 3
internal/ui/chat/guilds_tree.go

@@ -411,9 +411,11 @@ func (gt *guildsTree) HandleEvent(event tview.Event) tview.Command {
 			return nil
 		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.MoveToParentNode.Keybind):
 			return handler(tcell.NewEventKey(tcell.KeyRune, "K", tcell.ModNone))
-		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Up.Keybind):
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Up.Keybind),
+			keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.ArrowUp.Keybind):
 			return handler(tcell.NewEventKey(tcell.KeyUp, "", tcell.ModNone))
-		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Down.Keybind):
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Down.Keybind),
+			keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.ArrowDown.Keybind):
 			return handler(tcell.NewEventKey(tcell.KeyDown, "", tcell.ModNone))
 		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.Top.Keybind):
 			gt.Move(gt.GetRowCount() * -1)
@@ -431,7 +433,8 @@ func (gt *guildsTree) HandleEvent(event tview.Event) tview.Command {
 				return cmd
 			}
 			return gt.chat.focusMessagesList()
-		case event.Key() == tcell.KeyRight:
+		case keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.ArrowLeft.Keybind),
+			keybind.Matches(event, gt.cfg.Keybinds.GuildsTree.ArrowRight.Keybind):
 			return gt.chat.focusMessagesList()
 		}
 		// Do not fall through to TreeView defaults for unmatched keys.

+ 9 - 6
internal/ui/chat/messages_list.go

@@ -662,16 +662,19 @@ func (ml *messagesList) HandleEvent(event tview.Event) tview.Command {
 		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.Cancel.Keybind):
 			ml.clearSelection()
 			return tview.SetFocus(ml.chatView.guildsTree)
-		case event.Key() == tcell.KeyLeft:
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind),
+			keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ArrowUp.Keybind):
+			return ml.selectUp()
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind),
+			keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ArrowDown.Keybind):
+			ml.selectDown()
+			return nil
+		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ArrowLeft.Keybind),
+			keybind.Matches(event, ml.cfg.Keybinds.MessagesList.ArrowRight.Keybind):
 			if cmd := ml.chatView.focusGuildsTree(); cmd != nil {
 				return cmd
 			}
 			return nil
-		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectUp.Keybind):
-			return ml.selectUp()
-		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectDown.Keybind):
-			ml.selectDown()
-			return nil
 		case keybind.Matches(event, ml.cfg.Keybinds.MessagesList.SelectTop.Keybind):
 			ml.selectTop()
 			return nil