fix(dmenu): use rofi/fuzzel info field for URL storage
- commit
- 645dfb3a8bf1ac7c649d256310f531ed0eeccdd2
- author
- bnema · · 2025-12-18 20:04 UTC
- parent
- 3a58730
- signature
- unsigned
1 file · +46 -25
M internal/cli/cmd/dmenu.go +46 -25
| @@ -107,7 +107,7 @@ func runDmenuPipe() error { | |||
| 107 | 107 | return fmt.Errorf("get history: %w", err) | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 110 | - | // Output in rofi-compatible format | |
| 110 | + | // Output in rofi/fuzzel compatible format | |
| 111 | 111 | for _, entry := range entries { | |
| 112 | 112 | title := entry.Title | |
| 113 | 113 | if title == "" { | |
| @@ -119,27 +119,23 @@ func runDmenuPipe() error { | |||
| 119 | 119 | title = title[:77] + "..." | |
| 120 | 120 | } | |
| 121 | 121 | ||
| 122 | - | // Build output line based on config | |
| 123 | - | var parts []string | |
| 124 | - | parts = append(parts, cfg.HistoryPrefix+" "+title) | |
| 125 | - | ||
| 126 | - | if cfg.ShowVisitCount && entry.VisitCount > 0 { | |
| 127 | - | parts[0] = fmt.Sprintf("%s (%d)", parts[0], entry.VisitCount) | |
| 128 | - | } | |
| 122 | + | // Check if we have a favicon | |
| 123 | + | faviconPath := getFaviconPath(entry.URL) | |
| 129 | 124 | ||
| 130 | - | if cfg.ShowLastVisited && !entry.LastVisited.IsZero() { | |
| 131 | - | dateStr := entry.LastVisited.Format(cfg.DateFormat) | |
| 132 | - | parts[0] = fmt.Sprintf("%s [%s]", parts[0], dateStr) | |
| 125 | + | // Build display text | |
| 126 | + | var displayText string | |
| 127 | + | if faviconPath != "" { | |
| 128 | + | displayText = title | |
| 129 | + | } else { | |
| 130 | + | displayText = cfg.HistoryPrefix + " " + title | |
| 133 | 131 | } | |
| 134 | 132 | ||
| 135 | - | // Output with URL as the actual value (for selection) | |
| 136 | - | // Using tab separator so rofi can use -d '\t' | |
| 137 | - | // Format: EntryName\0icon\x1f/path/to/favicon\tURL (rofi/fuzzel compatible) | |
| 138 | - | faviconPath := getFaviconPath(entry.URL) | |
| 133 | + | // Format: "DisplayText\0icon\x1f/path\x1finfo\x1fURL\n" | |
| 134 | + | // Using info field to store URL for selection | |
| 139 | 135 | if faviconPath != "" { | |
| 140 | - | fmt.Printf("%s\x00icon\x1f%s\t%s\n", parts[0], faviconPath, entry.URL) | |
| 136 | + | fmt.Printf("%s\x00icon\x1f%s\x1finfo\x1f%s\n", displayText, faviconPath, entry.URL) | |
| 141 | 137 | } else { | |
| 142 | - | fmt.Printf("%s\t%s\n", parts[0], entry.URL) | |
| 138 | + | fmt.Printf("%s\x00info\x1f%s\n", displayText, entry.URL) | |
| 143 | 139 | } | |
| 144 | 140 | } | |
| 145 | 141 | ||
| @@ -159,14 +155,7 @@ func runDmenuSelect() error { | |||
| 159 | 155 | return nil // No selection | |
| 160 | 156 | } | |
| 161 | 157 | ||
| 162 | - | // If the line contains a tab, the URL is after it | |
| 163 | - | // Otherwise, try to extract URL or use the whole line | |
| 164 | - | url := line | |
| 165 | - | if idx := strings.Index(line, "\t"); idx != -1 { | |
| 166 | - | url = strings.TrimSpace(line[idx+1:]) | |
| 167 | - | } | |
| 168 | - | ||
| 169 | - | // Try to open the URL | |
| 158 | + | url := parseSelection(line) | |
| 170 | 159 | if url != "" { | |
| 171 | 160 | return openInBrowser(url) | |
| 172 | 161 | } | |
| @@ -174,6 +163,38 @@ func runDmenuSelect() error { | |||
| 174 | 163 | return nil | |
| 175 | 164 | } | |
| 176 | 165 | ||
| 166 | + | // parseSelection extracts the URL from a dmenu selection. | |
| 167 | + | func parseSelection(selection string) string { | |
| 168 | + | selection = strings.TrimSpace(selection) | |
| 169 | + | ||
| 170 | + | // Extract info field if present (format: "text\0...info\x1fURL") | |
| 171 | + | if infoIndex := strings.Index(selection, "info\x1f"); infoIndex > 0 { | |
| 172 | + | info := selection[infoIndex+5:] // Skip "info\x1f" | |
| 173 | + | // Info might have more fields after, but URL should be first | |
| 174 | + | if endIndex := strings.Index(info, "\x1f"); endIndex > 0 { | |
| 175 | + | return strings.TrimSpace(info[:endIndex]) | |
| 176 | + | } | |
| 177 | + | return strings.TrimSpace(info) | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // Strip metadata if present (format: "text\0...") | |
| 181 | + | if nullIndex := strings.Index(selection, "\x00"); nullIndex > 0 { | |
| 182 | + | selection = selection[:nullIndex] | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // Legacy: strip emoji prefix if present | |
| 186 | + | selection = strings.TrimPrefix(selection, "🕒 ") | |
| 187 | + | selection = strings.TrimPrefix(selection, "🔍 ") | |
| 188 | + | selection = strings.TrimPrefix(selection, "🌐 ") | |
| 189 | + | ||
| 190 | + | // If it looks like a URL, return it | |
| 191 | + | if strings.Contains(selection, "://") || strings.Contains(selection, ".") { | |
| 192 | + | return selection | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | return selection | |
| 196 | + | } | |
| 197 | + | ||
| 177 | 198 | // runDmenuInteractive runs the interactive TUI dmenu. | |
| 178 | 199 | func runDmenuInteractive() error { | |
| 179 | 200 | app := GetApp() | |
| @@ -107,7 +107,7 @@ func runDmenuPipe() error { | |||||
| 107 | return fmt.Errorf("get history: %w", err) | 107 | return fmt.Errorf("get history: %w", err) | ||
| 108 | } | 108 | } | ||
| 109 | 109 | ||||
| 110 | - | // Output in rofi-compatible format | 110 | + | // Output in rofi/fuzzel compatible format |
| 111 | for _, entry := range entries { | 111 | for _, entry := range entries { | ||
| 112 | title := entry.Title | 112 | title := entry.Title | ||
| 113 | if title == "" { | 113 | if title == "" { | ||
| @@ -119,27 +119,23 @@ func runDmenuPipe() error { | |||||
| 119 | title = title[:77] + "..." | 119 | title = title[:77] + "..." | ||
| 120 | } | 120 | } | ||
| 121 | 121 | ||||
| 122 | - | // Build output line based on config | 122 | + | // Check if we have a favicon |
| 123 | - | var parts []string | 123 | + | faviconPath := getFaviconPath(entry.URL) |
| 124 | - | parts = append(parts, cfg.HistoryPrefix+" "+title) | |||
| 125 | - | ||||
| 126 | - | if cfg.ShowVisitCount && entry.VisitCount > 0 { | |||
| 127 | - | parts[0] = fmt.Sprintf("%s (%d)", parts[0], entry.VisitCount) | |||
| 128 | - | } | |||
| 129 | 124 | ||||
| 130 | - | if cfg.ShowLastVisited && !entry.LastVisited.IsZero() { | 125 | + | // Build display text |
| 131 | - | dateStr := entry.LastVisited.Format(cfg.DateFormat) | 126 | + | var displayText string |
| 132 | - | parts[0] = fmt.Sprintf("%s [%s]", parts[0], dateStr) | 127 | + | if faviconPath != "" { |
| 128 | + | displayText = title | |||
| 129 | + | } else { | |||
| 130 | + | displayText = cfg.HistoryPrefix + " " + title | |||
| 133 | } | 131 | } | ||
| 134 | 132 | ||||
| 135 | - | // Output with URL as the actual value (for selection) | 133 | + | // Format: "DisplayText\0icon\x1f/path\x1finfo\x1fURL\n" |
| 136 | - | // Using tab separator so rofi can use -d '\t' | 134 | + | // Using info field to store URL for selection |
| 137 | - | // Format: EntryName\0icon\x1f/path/to/favicon\tURL (rofi/fuzzel compatible) | |||
| 138 | - | faviconPath := getFaviconPath(entry.URL) | |||
| 139 | if faviconPath != "" { | 135 | if faviconPath != "" { | ||
| 140 | - | fmt.Printf("%s\x00icon\x1f%s\t%s\n", parts[0], faviconPath, entry.URL) | 136 | + | fmt.Printf("%s\x00icon\x1f%s\x1finfo\x1f%s\n", displayText, faviconPath, entry.URL) |
| 141 | } else { | 137 | } else { | ||
| 142 | - | fmt.Printf("%s\t%s\n", parts[0], entry.URL) | 138 | + | fmt.Printf("%s\x00info\x1f%s\n", displayText, entry.URL) |
| 143 | } | 139 | } | ||
| 144 | } | 140 | } | ||
| 145 | 141 | ||||
| @@ -159,14 +155,7 @@ func runDmenuSelect() error { | |||||
| 159 | return nil // No selection | 155 | return nil // No selection | ||
| 160 | } | 156 | } | ||
| 161 | 157 | ||||
| 162 | - | // If the line contains a tab, the URL is after it | 158 | + | url := parseSelection(line) |
| 163 | - | // Otherwise, try to extract URL or use the whole line | |||
| 164 | - | url := line | |||
| 165 | - | if idx := strings.Index(line, "\t"); idx != -1 { | |||
| 166 | - | url = strings.TrimSpace(line[idx+1:]) | |||
| 167 | - | } | |||
| 168 | - | ||||
| 169 | - | // Try to open the URL | |||
| 170 | if url != "" { | 159 | if url != "" { | ||
| 171 | return openInBrowser(url) | 160 | return openInBrowser(url) | ||
| 172 | } | 161 | } | ||
| @@ -174,6 +163,38 @@ func runDmenuSelect() error { | |||||
| 174 | return nil | 163 | return nil | ||
| 175 | } | 164 | } | ||
| 176 | 165 | ||||
| 166 | + | // parseSelection extracts the URL from a dmenu selection. | |||
| 167 | + | func parseSelection(selection string) string { | |||
| 168 | + | selection = strings.TrimSpace(selection) | |||
| 169 | + | ||||
| 170 | + | // Extract info field if present (format: "text\0...info\x1fURL") | |||
| 171 | + | if infoIndex := strings.Index(selection, "info\x1f"); infoIndex > 0 { | |||
| 172 | + | info := selection[infoIndex+5:] // Skip "info\x1f" | |||
| 173 | + | // Info might have more fields after, but URL should be first | |||
| 174 | + | if endIndex := strings.Index(info, "\x1f"); endIndex > 0 { | |||
| 175 | + | return strings.TrimSpace(info[:endIndex]) | |||
| 176 | + | } | |||
| 177 | + | return strings.TrimSpace(info) | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // Strip metadata if present (format: "text\0...") | |||
| 181 | + | if nullIndex := strings.Index(selection, "\x00"); nullIndex > 0 { | |||
| 182 | + | selection = selection[:nullIndex] | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // Legacy: strip emoji prefix if present | |||
| 186 | + | selection = strings.TrimPrefix(selection, "🕒 ") | |||
| 187 | + | selection = strings.TrimPrefix(selection, "🔍 ") | |||
| 188 | + | selection = strings.TrimPrefix(selection, "🌐 ") | |||
| 189 | + | ||||
| 190 | + | // If it looks like a URL, return it | |||
| 191 | + | if strings.Contains(selection, "://") || strings.Contains(selection, ".") { | |||
| 192 | + | return selection | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | return selection | |||
| 196 | + | } | |||
| 197 | + | ||||
| 177 | // runDmenuInteractive runs the interactive TUI dmenu. | 198 | // runDmenuInteractive runs the interactive TUI dmenu. | ||
| 178 | func runDmenuInteractive() error { | 199 | func runDmenuInteractive() error { | ||
| 179 | app := GetApp() | 200 | app := GetApp() | ||