package mujica import ( "cmp" "fmt" "log/slog" "path" "slices" "time" "github.com/egoist/mygo/ui" "mujicaui/icons" "mujicaui/theme" ) // RecentFile is a file the user opened. type RecentFile struct { Path string Opened time.Time } // RecentFilesOptions configure a RecentFiles. type RecentFilesOptions struct { // Revision changes when files are edited in place. Revision uint64 // Now dates the groups; zero uses the frame's time. Now time.Time Disabled bool } // RecentFilesState is a RecentFiles' choice and place. type RecentFilesState struct { rows ui.ListState selected int flat []recentRow data *RecentFile n int day time.Time started bool revision uint64 } // recentRow holds a group header or file index. type recentRow struct{ group, file int } // RecentFilesResult is the list and what happened this frame. type RecentFilesResult struct { Element *ui.Element Opened *RecentFile // Removed identifies one file; Cleared requests clearing all files. Removed string Cleared bool } // RecentFiles groups recent files by time and supports clearing. func RecentFiles(c *ui.Context, files []RecentFile, s *RecentFilesState, opts RecentFilesOptions) RecentFilesResult { if s == nil { panic("mujica: RecentFiles needs a state") } if !s.started { s.started, s.selected = true, -1 } now := opts.Now if now.IsZero() { now = c.Now() } if s.revision != opts.Revision { s.day = time.Time{} s.revision = opts.Revision } recentGroup(files, s, now) k := Tokens(c) d := Density(c) var res RecentFilesResult res.Element = codePanel(c).Disabled(opts.Disabled) disabled := res.Element.IsDisabled() if disabled { copy := *s s = © } res.Element.Children(func() { codeHeader(c, func() { ui.Text(c, msg(c, "recent_files.title")).Font(theme.SerifFont).FontWeight(600).Grow(1) if len(files) > 0 && Button(c, msg(c, "recent_files.clear"), ButtonOptions{Variant: Ghost}).MinHeight(theme.MinHitSize).Clicked() && !disabled { res.Cleared = true slog.Debug("mujica: recent_files clear") } }) if len(files) == 0 { codeCaption(c, msg(c, "recent_files.empty")).Padding(d.Unit() * 4) return } s.rows.Selected = &s.selected s.rows.Key = func(i int) any { r := s.flat[i] if r.file < 0 { return r.group } return files[r.file].Path } s.rows.Header = func(i int) bool { return s.flat[i].file < 0 } s.rows.Label = func(i int) string { if r := s.flat[i]; r.file >= 0 { return path.Base(files[r.file].Path) } return recentGroupName(c, s.flat[i].group) } list := codeRows(c, &s.rows, len(s.flat), func(i int) { r := s.flat[i] if r.file < 0 { ui.Text(c, recentGroupName(c, r.group)).Font(theme.SerifFont).FontSize(FontSize(c, theme.CaptionSize)). TextColor(k.Ornament.Mix(k.Text, 0.2)).Grow(1) return } f := files[r.file] FileIcon(c, f.Path, FileIconOptions{}) ui.Column(c).Grow(1).MinWidth(0).Children(func() { ui.Text(c, path.Base(f.Path)).SingleLine() codeCaption(c, path.Dir(f.Path)) }) when := f.Opened.Format("15:04") if r.group >= 2 { when = f.Opened.Format("Jan 2") } codeCaption(c, when).Shrink(0) if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "recent_files.remove"), path.Base(f.Path))).Clicked() && !disabled { res.Removed = f.Path slog.Debug("mujica: recent_files remove") } }) list.Background(k.Surface) if list.Submitted() && !disabled && s.selected >= 0 && s.selected < len(s.flat) && s.flat[s.selected].file >= 0 { f := files[s.flat[s.selected].file] res.Opened = &f slog.Debug("mujica: recent_files open") } }) return res } func recentGroupName(c *ui.Context, g int) string { return msg(c, [...]string{"recent_files.today", "recent_files.yesterday", "recent_files.week", "recent_files.earlier"}[g]) } // recentGroup caches files sorted into relative-day groups. func recentGroup(files []RecentFile, s *RecentFilesState, now time.Time) { var first *RecentFile if len(files) > 0 { first = &files[0] } today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) if s.data == first && s.n == len(files) && s.day.Equal(today) { return } s.data, s.n, s.day = first, len(files), today idx := make([]int, len(files)) for i := range idx { idx[i] = i } slices.SortStableFunc(idx, func(a, b int) int { return cmp.Compare(files[b].Opened.UnixNano(), files[a].Opened.UnixNano()) }) group := func(t time.Time) int { switch { case !t.Before(today): return 0 case !t.Before(today.AddDate(0, 0, -1)): return 1 case !t.Before(today.AddDate(0, 0, -6)): return 2 } return 3 } s.flat = s.flat[:0] last := -1 for _, i := range idx { g := group(files[i].Opened) if g != last { s.flat = append(s.flat, recentRow{g, -1}) last = g } s.flat = append(s.flat, recentRow{g, i}) } }