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
3 changes: 1 addition & 2 deletions internal/claude/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ func listMarkdownFilesFS(fsys fs.FS, dir string) []string {

var result []string
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") {
name := strings.TrimSuffix(entry.Name(), ".md")
if name, ok := strings.CutSuffix(entry.Name(), ".md"); !entry.IsDir() && ok {
result = append(result, name)
}
}
Expand Down
25 changes: 7 additions & 18 deletions internal/tui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package tui

import (
"sort"
"cmp"
"maps"
"slices"
"strings"

tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -252,15 +254,6 @@ func firstScope(m map[claude.Scope]bool) claude.Scope {
return claude.ScopeNone
}

// copyMap creates a shallow copy of a scope map.
func copyMap(m map[claude.Scope]bool) map[claude.Scope]bool {
result := make(map[claude.Scope]bool, len(m))
for k, v := range m {
result[k] = v
}
return result
}

// MainState holds state for the main two-pane view.
type MainState struct {
pendingOps map[string]Operation
Expand Down Expand Up @@ -532,18 +525,14 @@ func processInstalledPlugin(p claude.InstalledPlugin, allScopes claude.ScopeStat
// sortAndGroupByMarketplace sorts and groups plugins by marketplace with headers.
func sortAndGroupByMarketplace(byMarketplace map[string][]PluginState) []PluginState {
// Sort marketplace names
marketplaces := make([]string, 0, len(byMarketplace))
for marketplace := range byMarketplace {
marketplaces = append(marketplaces, marketplace)
}
sort.Strings(marketplaces)
marketplaces := slices.Sorted(maps.Keys(byMarketplace))

// Build result with headers
var result []PluginState
for _, marketplace := range marketplaces {
plugins := byMarketplace[marketplace]
sort.Slice(plugins, func(i, j int) bool {
return strings.ToLower(plugins[i].Name) < strings.ToLower(plugins[j].Name)
slices.SortFunc(plugins, func(a, b PluginState) int {
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
result = append(result, PluginState{
Name: marketplace,
Expand Down Expand Up @@ -613,7 +602,7 @@ func (m *Model) openScopeDialog(pluginID string, installedScopes map[claude.Scop
m.mode = ModeScopeDialog
m.main.scopeDialog = scopeDialogState{
pluginID: pluginID,
originalScopes: copyMap(installedScopes),
originalScopes: maps.Clone(installedScopes),
}
// Initialize checkboxes from current installed scopes (presence, not enabled value)
_, m.main.scopeDialog.scopes[0] = installedScopes[claude.ScopeUser]
Expand Down
78 changes: 34 additions & 44 deletions internal/tui/update.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package tui

import (
"cmp"
"fmt"
"io/fs"
"maps"
"os"
"sort"
"slices"
"strings"

tea "github.com/charmbracelet/bubbletea"
Expand Down Expand Up @@ -225,15 +227,15 @@ func (m *Model) applyScopeDialogDelta() {
m.main.pendingOps[dialog.pluginID] = Operation{
PluginID: dialog.pluginID,
Scopes: uninstallScopes,
OriginalScopes: copyMap(original),
OriginalScopes: maps.Clone(original),
Type: OpUninstall,
}
case len(installScopes) > 0 && len(uninstallScopes) == 0:
// Pure install (adding scopes)
m.main.pendingOps[dialog.pluginID] = Operation{
PluginID: dialog.pluginID,
Scopes: installScopes,
OriginalScopes: copyMap(original),
OriginalScopes: maps.Clone(original),
Type: OpInstall,
}
default:
Expand All @@ -244,7 +246,7 @@ func (m *Model) applyScopeDialogDelta() {
PluginID: dialog.pluginID,
Scopes: installScopes,
UninstallScopes: uninstallScopes,
OriginalScopes: copyMap(original),
OriginalScopes: maps.Clone(original),
Type: OpScopeChange,
}
}
Expand Down Expand Up @@ -297,15 +299,15 @@ func (m *Model) moveDown() {
// pageUp moves up by a page.
func (m *Model) pageUp() {
pageSize := m.getPageSize()
for i := 0; i < pageSize; i++ {
for range pageSize {
m.moveUp()
}
}

// pageDown moves down by a page.
func (m *Model) pageDown() {
pageSize := m.getPageSize()
for i := 0; i < pageSize; i++ {
for range pageSize {
m.moveDown()
}
}
Expand Down Expand Up @@ -358,10 +360,7 @@ func (m *Model) ensureVisible() {
if m.listOffset < 0 {
m.listOffset = 0
}
maxOffset := len(m.plugins) - pageSize
if maxOffset < 0 {
maxOffset = 0
}
maxOffset := max(len(m.plugins)-pageSize, 0)
if m.listOffset > maxOffset {
m.listOffset = maxOffset
}
Expand Down Expand Up @@ -400,7 +399,7 @@ func (m *Model) selectForInstall(scope claude.Scope) {
m.main.pendingOps[plugin.ID] = Operation{
PluginID: plugin.ID,
Scopes: []claude.Scope{scope},
OriginalScopes: copyMap(plugin.InstalledScopes),
OriginalScopes: maps.Clone(plugin.InstalledScopes),
Type: OpMigrate,
}
continue
Expand Down Expand Up @@ -455,7 +454,7 @@ func (m *Model) computeNextToggleOp(plugin *PluginState) *Operation {
return &Operation{
PluginID: plugin.ID,
Scopes: []claude.Scope{},
OriginalScopes: copyMap(plugin.InstalledScopes),
OriginalScopes: maps.Clone(plugin.InstalledScopes),
Type: OpUninstall,
}
}
Expand All @@ -473,7 +472,7 @@ func (m *Model) firstToggleOp(plugin *PluginState, scope claude.Scope) *Operatio
return &Operation{
PluginID: plugin.ID,
Scopes: []claude.Scope{scope},
OriginalScopes: copyMap(plugin.InstalledScopes),
OriginalScopes: maps.Clone(plugin.InstalledScopes),
Type: OpMigrate,
}
}
Expand Down Expand Up @@ -514,7 +513,7 @@ func (m *Model) selectForUninstall() {
m.main.pendingOps[plugin.ID] = Operation{
PluginID: plugin.ID,
Scopes: []claude.Scope{plugin.SingleScope()},
OriginalScopes: copyMap(plugin.InstalledScopes),
OriginalScopes: maps.Clone(plugin.InstalledScopes),
Type: OpUninstall,
}
}
Expand Down Expand Up @@ -587,7 +586,7 @@ func (m *Model) startExecution() (tea.Model, tea.Cmd) {
}

// Sort operations: uninstalls first, then migrations, then scope changes, then updates, then installs, then enable/disable
sort.Slice(m.progress.operations, func(i, j int) bool {
slices.SortFunc(m.progress.operations, func(a, b Operation) int {
typeOrder := map[OperationType]int{
OpUninstall: 0,
OpMigrate: 1,
Expand All @@ -597,14 +596,7 @@ func (m *Model) startExecution() (tea.Model, tea.Cmd) {
OpEnable: 5,
OpDisable: 6,
}
orderI := typeOrder[m.progress.operations[i].Type]
orderJ := typeOrder[m.progress.operations[j].Type]

// If same order, maintain stable sort (don't swap)
if orderI == orderJ {
return false
}
return orderI < orderJ
return cmp.Compare(typeOrder[a.Type], typeOrder[b.Type])
})

m.progress.currentIdx = 0
Expand Down Expand Up @@ -943,11 +935,11 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
case tea.MouseButtonLeft:
m.handleMouseClick(msg)
case tea.MouseButtonWheelUp:
for i := 0; i < wheelScrollSpeed; i++ {
for range wheelScrollSpeed {
m.moveUp()
}
case tea.MouseButtonWheelDown:
for i := 0; i < wheelScrollSpeed; i++ {
for range wheelScrollSpeed {
m.moveDown()
}
}
Expand Down Expand Up @@ -1031,12 +1023,12 @@ func (m *Model) extractNonHeaderPlugins() []PluginState {
func applySortMode(plugins []PluginState, sortMode SortMode) {
switch sortMode {
case SortByNameAsc:
sort.Slice(plugins, func(i, j int) bool {
return strings.ToLower(plugins[i].Name) < strings.ToLower(plugins[j].Name)
slices.SortFunc(plugins, func(a, b PluginState) int {
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
case SortByNameDesc:
sort.Slice(plugins, func(i, j int) bool {
return strings.ToLower(plugins[i].Name) > strings.ToLower(plugins[j].Name)
slices.SortFunc(plugins, func(a, b PluginState) int {
return cmp.Compare(strings.ToLower(b.Name), strings.ToLower(a.Name))
})
case SortByScope:
sortByScope(plugins)
Expand All @@ -1053,23 +1045,21 @@ func sortByScope(plugins []PluginState) {
claude.ScopeUser: 2,
claude.ScopeNone: 3,
}
sort.Slice(plugins, func(i, j int) bool {
orderI := scopeOrder[plugins[i].SingleScope()]
orderJ := scopeOrder[plugins[j].SingleScope()]
if orderI != orderJ {
return orderI < orderJ
slices.SortFunc(plugins, func(a, b PluginState) int {
if c := cmp.Compare(scopeOrder[a.SingleScope()], scopeOrder[b.SingleScope()]); c != 0 {
return c
}
return strings.ToLower(plugins[i].Name) < strings.ToLower(plugins[j].Name)
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
}

// sortByMarketplace sorts plugins by marketplace name.
func sortByMarketplace(plugins []PluginState) {
sort.Slice(plugins, func(i, j int) bool {
if plugins[i].Marketplace != plugins[j].Marketplace {
return plugins[i].Marketplace < plugins[j].Marketplace
slices.SortFunc(plugins, func(a, b PluginState) int {
if c := cmp.Compare(a.Marketplace, b.Marketplace); c != 0 {
return c
}
return strings.ToLower(plugins[i].Name) < strings.ToLower(plugins[j].Name)
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
}

Expand Down Expand Up @@ -1157,7 +1147,7 @@ func rebuildWithGroupHeaders(plugins []PluginState, sortMode SortMode) []PluginS
}
byGroup[group] = append(byGroup[group], p)
}
sort.Strings(groups) // Sort groups alphabetically
slices.Sort(groups) // Sort groups alphabetically
for _, group := range groups {
result = append(result, PluginState{
Name: group,
Expand All @@ -1167,12 +1157,12 @@ func rebuildWithGroupHeaders(plugins []PluginState, sortMode SortMode) []PluginS
// Sort within group by current sort mode
groupPlugins := byGroup[group]
if sortMode == SortByNameDesc {
sort.Slice(groupPlugins, func(i, j int) bool {
return strings.ToLower(groupPlugins[i].Name) > strings.ToLower(groupPlugins[j].Name)
slices.SortFunc(groupPlugins, func(a, b PluginState) int {
return cmp.Compare(strings.ToLower(b.Name), strings.ToLower(a.Name))
})
} else {
sort.Slice(groupPlugins, func(i, j int) bool {
return strings.ToLower(groupPlugins[i].Name) < strings.ToLower(groupPlugins[j].Name)
slices.SortFunc(groupPlugins, func(a, b PluginState) int {
return cmp.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
})
}
result = append(result, groupPlugins...)
Expand Down
45 changes: 13 additions & 32 deletions internal/tui/view.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package tui

import (
"cmp"
"fmt"
"sort"
"maps"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -73,10 +75,7 @@ func (m *Model) renderList(styles Styles) string {

// Calculate visible range
start := m.listOffset
end := start + visibleHeight
if end > len(plugins) {
end = len(plugins)
}
end := min(start+visibleHeight, len(plugins))

for i := start; i < end; i++ {
plugin := plugins[i]
Expand Down Expand Up @@ -189,7 +188,7 @@ func (m *Model) getScopeIndicator(plugin PluginState, styles Styles) string {

// applyScopeDelta returns a new scope set with uninstalls removed and installs added.
func applyScopeDelta(base map[claude.Scope]bool, uninstall, install []claude.Scope) map[claude.Scope]bool {
result := copyMap(base)
result := maps.Clone(base)
for _, s := range uninstall {
delete(result, s)
}
Expand Down Expand Up @@ -478,19 +477,13 @@ func (m *Model) renderDoc(styles Styles) string {
visibleHeight := m.height - 4 // Account for header and help bar

// Clamp scroll position
maxScroll := len(lines) - visibleHeight
if maxScroll < 0 {
maxScroll = 0
}
maxScroll := max(len(lines)-visibleHeight, 0)
if m.doc.scroll > maxScroll {
m.doc.scroll = maxScroll
}

// Get visible lines
endIdx := m.doc.scroll + visibleHeight
if endIdx > len(lines) {
endIdx = len(lines)
}
endIdx := min(m.doc.scroll+visibleHeight, len(lines))
visibleLines := lines[m.doc.scroll:endIdx]
content := strings.Join(visibleLines, "\n")

Expand Down Expand Up @@ -527,7 +520,7 @@ func (m *Model) renderConfirmation(styles Styles) string {

// Sort operations by type for consistent display
// Uninstalls, then migrations, then updates, then installs, then enables, then disables
sort.Slice(operations, func(i, j int) bool {
slices.SortFunc(operations, func(a, b Operation) int {
typeOrder := map[OperationType]int{
OpUninstall: 0,
OpMigrate: 1,
Expand All @@ -536,7 +529,7 @@ func (m *Model) renderConfirmation(styles Styles) string {
OpEnable: 4,
OpDisable: 5,
}
return typeOrder[operations[i].Type] < typeOrder[operations[j].Type]
return cmp.Compare(typeOrder[a.Type], typeOrder[b.Type])
})

for _, op := range operations {
Expand Down Expand Up @@ -795,7 +788,7 @@ func (m *Model) renderScopeDialog(styles Styles) string {
lines = append(lines, styles.Header.Render(" Scopes for "+dialog.pluginID+" "))
lines = append(lines, "")

for i := 0; i < 3; i++ {
for i := range 3 {
checkbox := "[ ]"
if dialog.scopes[i] {
checkbox = "[x]"
Expand Down Expand Up @@ -841,24 +834,12 @@ func (m *Model) renderConfig(styles Styles) string {
header := styles.Header.Render(" " + m.config.title + " ")

// Content area
contentHeight := m.height - 4 // Account for header and help bar
if contentHeight < 1 {
contentHeight = 1
}
contentHeight := max(m.height-4, 1) // Account for header and help bar

// Split content into lines and apply scroll
lines := strings.Split(m.config.content, "\n")
startLine := m.config.scroll
if startLine >= len(lines) {
startLine = len(lines) - 1
if startLine < 0 {
startLine = 0
}
}
endLine := startLine + contentHeight
if endLine > len(lines) {
endLine = len(lines)
}
startLine := min(m.config.scroll, max(len(lines)-1, 0))
endLine := min(startLine+contentHeight, len(lines))

visibleLines := lines[startLine:endLine]
content := strings.Join(visibleLines, "\n")
Expand Down
Loading