package git import ( "fmt" "log/slog" "path" "github.com/egoist/mygo/ui" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/data" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons" "git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme" ) // ChangedFile is a changed file in the tree or index. type ChangedFile struct { Path string Status GitStatus // Staged puts the file in the index group. Staged bool } // ChangesListState is a ChangesList's selection and grouping. type ChangesListState struct { list ui.ListState cursor int // rows maps rows to files; negatives are group headers. rows []int staged int src *ChangedFile n int started bool } // Selected returns the chosen file's index, or -1. func (s *ChangesListState) Selected() int { if s.cursor < 0 || s.cursor >= len(s.rows) { return -1 } return s.rows[s.cursor] } // ChangesListOptions configure a ChangesList. type ChangesListOptions struct { Disabled bool } // ChangesList groups staged and unstaged files; give it a height. func ChangesList(c *ui.Context, s *ChangesListState, files []ChangedFile, opts ChangesListOptions) GitListResult { if !s.started { s.started, s.cursor = true, -1 } var first *ChangedFile if len(files) > 0 { first = &files[0] } if first != s.src || len(files) != s.n { s.rows, s.staged = changesListRows(files) s.src, s.n, s.cursor = first, len(files), -1 slog.Debug("mujica: changes list regroup", "files", len(files), "staged", s.staged) } k := core.Tokens(c) var r GitListResult s.list.Selected = &s.cursor s.list.Header = func(i int) bool { return s.rows[i] < 0 } s.list.Key = func(i int) any { if f := s.rows[i]; f >= 0 { return fmt.Sprint(files[f].Staged, files[f].Path) } return s.rows[i] } s.list.Label = func(i int) string { if f := s.rows[i]; f >= 0 { return files[f].Path } return "" } prev := s.cursor outer := ui.Column(c).AlignItems(ui.Stretch).Disabled(opts.Disabled) var e *ui.Element outer.Children(func() { restore := data.DataSelectTheme(c, 0) defer restore() e = ui.List(c, &s.list, len(s.rows), func(i int) { changesListRow(c, s, files, i, &r) }) e.Grow(1).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius) }) r.Element, r.changed, r.submitted = outer, e.Changed(), e.Submitted() if outer.IsDisabled() { s.cursor, r.changed, r.submitted, r.action, r.item = prev, false, false, "", "" } if r.action != "" { slog.Debug("mujica: changes list action", "action", r.action, "path", r.item) } return r } // changesListRow builds row i: a header or a file. func changesListRow(c *ui.Context, s *ChangesListState, files []ChangedFile, i int, r *GitListResult) { k := core.Tokens(c) d := core.Density(c) u := d.Unit() f := s.rows[i] row := ui.Row(c).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2, 0, u*3) if f < 0 { key, all, icon, count := "changes_list.staged", "unstage_all", "minus", s.staged if f == -2 { key, all, icon, count = "changes_list.unstaged", "stage_all", "plus", len(files)-s.staged } row.Background(k.Background).Children(func() { ui.Text(c, fmt.Sprintf(core.Msg(c, key), count)).Font(theme.SerifFont).FontWeight(600). FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).Grow(1) b := core.IconAction(c, icons.Must(icon), core.Msg(c, "changes_list."+all)).Disabled(count == 0) if b.Tooltip(core.Msg(c, "changes_list."+all)).Clicked() && !b.IsDisabled() { r.action = all } }) return } cf := files[f] if s.cursor == i { row.Draw(func(p *ui.Painter, rc ui.Rect) { p.Fill(ui.Rect{X: rc.X, Y: rc.Y + 4, W: 3, H: rc.H - 8}, k.AccentText, 1.5) }) } row.Children(func() { GitStatusBadge(c, cf.Status, GitStatusBadgeOptions{}) dir := path.Dir(cf.Path) ui.Text(c, path.Base(cf.Path)).SingleLine().Shrink(0).MaxWidth(240) if dir != "." { ui.Text(c, dir).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine().MinWidth(0) } ui.Spacer(c) id, icon := "stage", "plus" if cf.Staged { id, icon = "unstage", "minus" } b := core.IconAction(c, icons.Must(icon), core.Msg(c, "changes_list."+id)+" "+cf.Path).Tooltip(core.Msg(c, "changes_list."+id)) if b.Clicked() && !b.IsDisabled() { r.action, r.item = id, cf.Path } }) } // changesListRows orders staged files, then unstaged, each under a header. func changesListRows(files []ChangedFile) ([]int, int) { rows := []int{-1} for i, f := range files { if f.Path == "" { panic("mujica: ChangedFile needs a Path") } gitStatusIndex(f.Status) if f.Staged { rows = append(rows, i) } } staged := len(rows) - 1 rows = append(rows, -2) for i, f := range files { if !f.Staged { rows = append(rows, i) } } return rows, staged }