diff --git a/cmd/slackdump/internal/ui/bubbles/btime/btime.go b/cmd/slackdump/internal/ui/bubbles/btime/btime.go index d13fc86c..9503ae01 100644 --- a/cmd/slackdump/internal/ui/bubbles/btime/btime.go +++ b/cmd/slackdump/internal/ui/bubbles/btime/btime.go @@ -24,7 +24,7 @@ type KeyMap struct { Quit key.Binding } -type TimeModel struct { +type Model struct { Time time.Time entry [6]int maxnum [3]int @@ -68,8 +68,8 @@ func DefaultStyles() Styles { } } -func NewTime(t time.Time) *TimeModel { - tm := &TimeModel{ +func New(t time.Time) *Model { + tm := &Model{ Time: t, entry: [6]int{0, 0, 0, 0, 0, 0}, maxnum: [3]int{23, 59, 59}, @@ -83,17 +83,21 @@ func NewTime(t time.Time) *TimeModel { return tm } -func (m *TimeModel) Focus() { +func (m *Model) Focus() { m.Focused = true } -func (m *TimeModel) Init() tea.Cmd { +func (m *Model) Blur() { + m.Focused = false +} + +func (m *Model) Init() tea.Cmd { return nil } var digitsRe = regexp.MustCompile(`\d`) -func (m *TimeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *Model) Update(msg tea.Msg) (*Model, tea.Cmd) { if !m.Focused { return m, nil } @@ -178,21 +182,26 @@ func (m *TimeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m *TimeModel) whatIf(digit int, hasVal int) int { +func (m *Model) whatIf(digit int, hasVal int) int { whatIf := make([]int, len(m.entry)) copy(whatIf, m.entry[:]) whatIf[digit] = hasVal return tupleVal(whatIf, m.cursor/2) } -func (m *TimeModel) updateTime() { +func (m *Model) updateTime() { hour := tupleVal(m.entry[:], 0) minute := tupleVal(m.entry[:], 1) second := tupleVal(m.entry[:], 2) m.Time = time.Date(m.Time.Year(), m.Time.Month(), m.Time.Day(), hour, minute, second, 0, time.UTC) } -func (m *TimeModel) toEntry() { +func (m *Model) Value() time.Time { + m.updateTime() + return m.Time +} + +func (m *Model) toEntry() { hour := m.Time.Hour() minute := m.Time.Minute() second := m.Time.Second() @@ -211,7 +220,7 @@ func tupleVal(entry []int, tuple int) int { return entry[tuple*2]*10 + entry[tuple*2+1] } -func (m *TimeModel) View() string { +func (m *Model) View() string { if m.finishing { return "" } @@ -231,9 +240,9 @@ func (m *TimeModel) View() string { ) var buf strings.Builder - buf.WriteString(cursor(m.cursor, 2, '↑') + "\n") + buf.WriteString(drawCursor(m.cursor, 2, '↑', 3) + "\n") buf.WriteString(r(0) + r(1) + sep + r(2) + r(3) + sep + r(4) + r(5) + "\n") - buf.WriteString(cursor(m.cursor, 2, '↓')) + buf.WriteString(drawCursor(m.cursor, 2, '↓', 3)) if m.ShowHelp { buf.WriteString("\n\n" + m.Styles.Help.Render( "↓/↑ change, tab jump, backspace zero, delete clear, enter to finish", @@ -243,14 +252,14 @@ func (m *TimeModel) View() string { return buf.String() } -func cursor(pos int, tupleSz int, char rune) string { +// numTuples is the size of the field in tuples. +func drawCursor(pos int, tupleSz int, char rune, numTuples int) string { var buf strings.Builder - numTuples := pos / tupleSz - offset := pos % tupleSz + const fill = " " - for i := 0; i < numTuples; i++ { - buf.WriteString(strings.Repeat(" ", tupleSz) + " ") - } - buf.WriteString(strings.Repeat(" ", offset) + string(char)) + before := pos + (pos / tupleSz) + after := numTuples*tupleSz - before + 1 + + buf.WriteString(strings.Repeat(fill, before) + string(char) + strings.Repeat(fill, after)) return buf.String() } diff --git a/cmd/slackdump/internal/ui/cfgui/updaters/date.go b/cmd/slackdump/internal/ui/cfgui/updaters/date.go index 8dc65e73..160e41b8 100644 --- a/cmd/slackdump/internal/ui/cfgui/updaters/date.go +++ b/cmd/slackdump/internal/ui/cfgui/updaters/date.go @@ -5,22 +5,28 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" datepicker "github.com/ethanefung/bubble-datepicker" + "github.com/rusq/slackdump/v3/cmd/slackdump/internal/ui/bubbles/btime" ) type DateModel struct { Value *time.Time dm datepicker.Model + tm *btime.Model finishing bool timeEnabled bool + state state } func NewDTTM(ptrTime *time.Time) DateModel { m := datepicker.New(*ptrTime) + t := btime.New(m.Time) m.SelectDate() return DateModel{ Value: ptrTime, dm: m, + tm: t, timeEnabled: true, } } @@ -29,6 +35,13 @@ func (m DateModel) Init() tea.Cmd { return m.dm.Init() } +type state int + +const ( + scalendar state = iota + stime +) + func (m DateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd @@ -39,23 +52,57 @@ func (m DateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "esc", "ctrl+c": return m, OnClose case "enter": - *m.Value = m.dm.Time + d := m.dm.Time + t := m.tm.Value() + *m.Value = time.Date(d.Year(), d.Month(), d.Day(), t.Hour(), t.Minute(), t.Second(), 0, m.Value.Location()) m.finishing = true return m, OnClose + case "tab": + switch m.state { + case scalendar: + if !m.timeEnabled || m.dm.Focused != datepicker.FocusCalendar { + break + } + m.state = stime + m.tm.Focus() + return m, nil + case stime: + // ignore tab in time mode. + return m, nil + } + case "shift+tab": + switch m.state { + case scalendar: + break + case stime: + m.state = scalendar + m.tm.Blur() + return m, nil + } } } - m.dm, cmd = m.dm.Update(msg) + switch m.state { + case scalendar: + m.dm, cmd = m.dm.Update(msg) + case stime: + m.tm, cmd = m.tm.Update(msg) + } cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } func (m DateModel) View() string { + if m.finishing { + return "" + } + var b strings.Builder - b.WriteString(m.dm.View()) if m.timeEnabled { - b.WriteString("\n\nTime: " + m.Value.Format("15:04:05") + " (UTC)") + b.WriteString(lipgloss.JoinVertical(lipgloss.Center, m.dm.View(), m.tm.View())) + } else { + b.WriteString(m.dm.View()) } b.WriteString("\n\n" + m.dm.Styles.Text.Render("Use arrow keys to navigate, tab/shift+tab to switch between fields, and enter to select.")) return b.String() diff --git a/cmd/slackdump/internal/ui/cfgui/updaters/examples/time/main.go b/cmd/slackdump/internal/ui/cfgui/updaters/examples/time/main.go index 72f03465..df35b253 100644 --- a/cmd/slackdump/internal/ui/cfgui/updaters/examples/time/main.go +++ b/cmd/slackdump/internal/ui/cfgui/updaters/examples/time/main.go @@ -19,12 +19,24 @@ func main() { defer logf.Close() log.SetOutput(logf) - m := btime.NewTime(time.Now()) + m := btime.New(time.Now()) m.Focused = true m.ShowHelp = true - p, err := tea.NewProgram(m).Run() + p, err := tea.NewProgram(timeModel{m}).Run() if err != nil { log.Fatal(err) } _ = p } + +type timeModel struct { + *btime.Model +} + +// Update wraps the Update method of the embedded btime.Model, to satisfy +// the tea.Model interface. +func (m timeModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + mod, cmd := m.Model.Update(msg) + m.Model = mod + return m, cmd +} diff --git a/cmd/slackdump/internal/ui/cfgui/updaters/filepick.go b/cmd/slackdump/internal/ui/cfgui/updaters/filepick.go index a06bd950..6aeaaef6 100644 --- a/cmd/slackdump/internal/ui/cfgui/updaters/filepick.go +++ b/cmd/slackdump/internal/ui/cfgui/updaters/filepick.go @@ -40,7 +40,6 @@ func (m FileModel) Init() tea.Cmd { } func (m FileModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - // cfg.Log.Printf("fileUpdateModel.Update: %[1]T %[1]v", msg) var cmd tea.Cmd var cmds []tea.Cmd