diff --git a/go.mod b/go.mod index 84dfa2cc0a6e139bfb97a2d4b1ee09d11b7fe71c..4d2ca0fe7f8fdedae6d8ed700ab03d058fdd2472 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/a-h/templ v0.3.1020 github.com/alecthomas/assert/v2 v2.11.0 github.com/alecthomas/chroma/v2 v2.27.0 - github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/ssh v0.0.0-20250826160808-ebfa259c7309 github.com/charmbracelet/wish v1.4.7 github.com/dustin/go-humanize v1.0.1 @@ -34,6 +33,7 @@ github.com/andybalholm/brotli v1.1.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/keygen v0.5.4 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect @@ -41,10 +41,8 @@ github.com/charmbracelet/log v1.0.0 // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/conpty v0.2.0 // indirect - github.com/charmbracelet/x/input v0.3.4 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.2.0 // indirect github.com/cli/browser v1.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect diff --git a/go.mod.sri b/go.mod.sri index 8619d03f24c6afdbff0414d086b64d31745dbb9d..0a4c6f04e07d90192484c28c81317d46870f2830 100644 --- a/go.mod.sri +++ b/go.mod.sri @@ -1 +1 @@ -sha256-i5ALTkq66Idtb4Z7+fadV08G6xOqERBYjqMb7c+dRdI= \ No newline at end of file +sha256-1r7CDfQqopP/0bSfqINrJcH3pwkwxW7G0DD4G1sAsLk= \ No newline at end of file diff --git a/go.sum b/go.sum index 172106093375fd38eacc1c357c532d46535a04ba..0143b8596d5c9edebd42d55a222888808a3d6f61 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819 h1:Zh+Ur3OsoWpvALHPLT45nOekHkgOt+IOfutBbPqM17I= +cuelabs.dev/go/oci/ociregistry v0.0.0-20251212221603-3adeb8663819/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= cuelabs.dev/go/oci/ociregistry v0.0.0-20260601085548-328ff8e2c943 h1:XUtzi/yWlmuy8V6kkmVbbmirmUqcFe9Ce3gmEaHXf1Q= cuelabs.dev/go/oci/ociregistry v0.0.0-20260601085548-328ff8e2c943/go.mod h1:WjmQxb+W6nVNCgj8nXrF24lIz95AHwnSl36tpjDZSU8= +cuelang.org/go v0.16.1 h1:iPN1lHZd2J0hjcr8hfq9PnIGk7VfPkKFfxH4de+m9sE= +cuelang.org/go v0.16.1/go.mod h1:/aW3967FeWC5Hc1cDrN4Z4ICVApdMi83wO5L3uF/1hM= cuelang.org/go v0.17.1 h1:liOkxZDqTHrzq0USJX+6bMYOZ5PSf+wzvQr15AHpDCQ= cuelang.org/go v0.17.1/go.mod h1:xlly/o1wSLvxOsi5vkQGieU0rLOt7TvUIizOFtnxHRU= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= @@ -51,14 +55,10 @@ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/conpty v0.2.0 h1:eKtA2hm34qNfgJCDp/M6Dc0gLy7e07YEK4qAdNGOvVY= github.com/charmbracelet/x/conpty v0.2.0/go.mod h1:fexgUnVrZgw8scD49f6VSi0Ggj9GWYIrpedRthAwW/8= -github.com/charmbracelet/x/input v0.3.4 h1:Mujmnv/4DaitU0p+kIsrlfZl/UlmeLKw1wAP3e1fMN0= -github.com/charmbracelet/x/input v0.3.4/go.mod h1:JI8RcvdZWQIhn09VzeK3hdp4lTz7+yhiEdpEQtZN+2c= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= -github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw= -github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= @@ -111,8 +111,8 @@ github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= -github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= -github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -182,6 +182,8 @@ github.com/protocolbuffers/txtpbfmt v0.0.0-20260420112717-c39628bde8b5 h1:Mckui8l+Wqz2Ve7XQvsE8SbHNmDWu8NA7Xce5NFJ/kM= github.com/protocolbuffers/txtpbfmt v0.0.0-20260420112717-c39628bde8b5/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go index ece95a37179c22515ca67ad533f27cc6233537fc..e094092d5ee3f6c8cf5225af8e9c71f87dba72f2 100644 --- a/internal/ssh/ssh.go +++ b/internal/ssh/ssh.go @@ -6,7 +6,6 @@ "log" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish" - bm "github.com/charmbracelet/wish/bubbletea" "github.com/charmbracelet/wish/logging" ) @@ -26,7 +25,6 @@ wish.WithAuthorizedKeys(settings.AuthorizedKeys), wish.WithAddress(fmt.Sprintf(":%d", settings.Port)), wish.WithHostKeyPath(settings.HostKey), wish.WithMiddleware( - bm.Middleware(tuiHandler(settings.RepoDir)), Middleware(settings.RepoDir, settings.CloneURL, settings.Port, hooks{}), logging.MiddlewareWithLogger(DefaultLogger), ), diff --git a/internal/ssh/tui.go b/internal/ssh/tui.go deleted file mode 100644 index 9fd362241879860a45a7dcd6e7330a4414568c00..0000000000000000000000000000000000000000 --- a/internal/ssh/tui.go +++ /dev/null @@ -1,258 +0,0 @@ -package ssh - -import ( - "fmt" - "os" - "path/filepath" - "slices" - "strings" - "unicode/utf8" - - "go.jolheiser.com/ugit/internal/git" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/ssh" - bm "github.com/charmbracelet/wish/bubbletea" -) - -func tuiHandler(repoDir string) bm.Handler { - return func(s ssh.Session) (tea.Model, []tea.ProgramOption) { - if _, _, ok := s.Pty(); !ok || len(s.Command()) > 0 { - return nil, nil - } - m := &model{repoDir: repoDir} - m.reload() - return m, []tea.ProgramOption{tea.WithAltScreen()} - } -} - -type mode int - -const ( - modeList mode = iota - modeEdit - modeDelete -) - -const ( - fieldDesc = iota - fieldPrivate -) - -type model struct { - repoDir string - repos []*git.Repo - cursor int - mode mode - err error - - // edit buffers - desc []rune - pos int // cursor position within desc - private bool - field int -} - -func (m *model) reload() { - m.repos = loadRepos(m.repoDir) - if m.cursor >= len(m.repos) { - m.cursor = max(len(m.repos)-1, 0) - } -} - -func loadRepos(repoDir string) []*git.Repo { - des, err := os.ReadDir(repoDir) - if err != nil { - return nil - } - var repos []*git.Repo - for _, de := range des { - if filepath.Ext(de.Name()) != ".git" { - continue - } - repo, err := git.NewRepo(repoDir, de.Name()) - if err != nil { - continue - } - repos = append(repos, repo) - } - return repos -} - -func (m *model) Init() tea.Cmd { return nil } - -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - key, ok := msg.(tea.KeyMsg) - if !ok { - return m, nil - } - if key.String() == "ctrl+c" { - return m, tea.Quit - } - switch m.mode { - case modeList: - return m.updateList(key) - case modeEdit: - return m.updateEdit(key) - case modeDelete: - return m.updateDelete(key) - } - return m, nil -} - -func (m *model) updateList(key tea.KeyMsg) (tea.Model, tea.Cmd) { - switch key.String() { - case "q": - return m, tea.Quit - case "up", "k": - if m.cursor > 0 { - m.cursor-- - } - case "down", "j": - if m.cursor < len(m.repos)-1 { - m.cursor++ - } - case "enter": - if len(m.repos) > 0 { - r := m.repos[m.cursor] - m.desc, m.private, m.field = []rune(r.Meta.Description), r.Meta.Private, fieldDesc - m.pos = len(m.desc) - m.err = nil - m.mode = modeEdit - } - case "d": - if len(m.repos) > 0 { - m.err = nil - m.mode = modeDelete - } - } - return m, nil -} - -func (m *model) updateEdit(key tea.KeyMsg) (tea.Model, tea.Cmd) { - switch key.Type { - case tea.KeyEsc: - m.mode = modeList - case tea.KeyUp, tea.KeyDown, tea.KeyTab, tea.KeyShiftTab: - m.field = (m.field + 1) % 2 - case tea.KeyEnter: - r := m.repos[m.cursor] - r.Meta.Description, r.Meta.Private = string(m.desc), m.private - m.err = r.SaveMeta() - m.mode = modeList - case tea.KeyLeft: - if m.field == fieldDesc && m.pos > 0 { - m.pos-- - } - case tea.KeyRight: - if m.field == fieldDesc && m.pos < len(m.desc) { - m.pos++ - } - case tea.KeyHome, tea.KeyCtrlA: - if m.field == fieldDesc { - m.pos = 0 - } - case tea.KeyEnd, tea.KeyCtrlE: - if m.field == fieldDesc { - m.pos = len(m.desc) - } - case tea.KeyBackspace: - if m.field == fieldDesc && m.pos > 0 { - m.desc = slices.Delete(m.desc, m.pos-1, m.pos) - m.pos-- - } - case tea.KeyDelete: - if m.field == fieldDesc && m.pos < len(m.desc) { - m.desc = slices.Delete(m.desc, m.pos, m.pos+1) - } - case tea.KeySpace: - if m.field == fieldPrivate { - m.private = !m.private - } else { - m.insert([]rune{' '}) - } - case tea.KeyRunes: - if m.field == fieldDesc { - m.insert(key.Runes) - } - } - return m, nil -} - -func (m *model) insert(rs []rune) { - m.desc = slices.Insert(m.desc, m.pos, rs...) - m.pos += len(rs) -} - -func (m *model) updateDelete(key tea.KeyMsg) (tea.Model, tea.Cmd) { - if key.String() == "y" { - m.err = os.RemoveAll(m.repos[m.cursor].Path()) - m.reload() - } - m.mode = modeList - return m, nil -} - -func (m *model) View() string { - var b strings.Builder - b.WriteString("ugit\n\n") - - if m.mode == modeEdit { - r := m.repos[m.cursor] - fmt.Fprintf(&b, "Editing %s\n\n", r.Name()) - cur := [2]string{" ", " "} - cur[m.field] = "> " - desc := string(m.desc) - if m.field == fieldDesc { - cursor := " " - if m.pos < len(m.desc) { - cursor = string(m.desc[m.pos]) - } - desc = string(m.desc[:m.pos]) + "\x1b[7m" + cursor + "\x1b[0m" - if m.pos < len(m.desc) { - desc += string(m.desc[m.pos+1:]) - } - } - fmt.Fprintf(&b, "%sDescription: %s\n", cur[fieldDesc], desc) - fmt.Fprintf(&b, "%sPrivate: %s\n", cur[fieldPrivate], checkbox(m.private)) - b.WriteString("\n↑/↓: next field • ←/→: move • space: toggle • enter: save • esc: cancel\n") - return b.String() - } - - if len(m.repos) == 0 { - b.WriteString("No repos.\n") - } - width := 0 - for _, r := range m.repos { - width = max(width, utf8.RuneCountInString(r.Name())) - } - for i, r := range m.repos { - cursor := " " - if i == m.cursor { - cursor = "> " - } - vis := "🔓" - if r.Meta.Private { - vis = "🔒" - } - fmt.Fprintf(&b, "%s%s %-*s %s\n", cursor, vis, width, r.Name(), r.Meta.Description) - } - - b.WriteString("\n") - if m.err != nil { - fmt.Fprintf(&b, "error: %v\n", m.err) - } - if m.mode == modeDelete { - fmt.Fprintf(&b, "Delete %s? (y/N)\n", m.repos[m.cursor].Name()) - } else { - b.WriteString("↑/↓: move • enter: edit • d: delete • q: quit\n") - } - return b.String() -} - -func checkbox(v bool) string { - if v { - return "[x]" - } - return "[ ]" -} diff --git a/internal/ssh/wish.go b/internal/ssh/wish.go index ec76abe30ada129fde479cb7580c12121ab622ab..64f6be07be25325f7b183186cad75b9bfce16ca6 100644 --- a/internal/ssh/wish.go +++ b/internal/ssh/wish.go @@ -109,8 +109,7 @@ } } // Repo list - // Interactive sessions get the TUI instead - if _, _, isPty := s.Pty(); len(cmd) == 0 && !isPty { + if len(cmd) == 0 { des, err := os.ReadDir(repoDir) if err != nil && err != fs.ErrNotExist { slog.Error("invalid repository", "error", err)