Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: add basic gum date command #683

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,22 @@ See [`charmbracelet/log`](https://github.com/charmbracelet/log) for more usage.

<img src="https://vhs.charm.sh/vhs-6jupuFM0s2fXiUrBE0I1vU.gif" width="600" alt="Running gum log with debug and error levels" />

## Date

Pick a date, starting from the current date by default:

```bash
gum date
```

Or starting from an initial date of your choosing:

```bash
gum date --value 2023-11-28
```

<img src="https://vhs.charm.sh/vhs-2Gkiemx0ALZZBmODcSrg2I.gif" width="600" alt="Running gum date with a prompt specified" />

## Examples

How to use `gum` in your daily workflows:
Expand Down
43 changes: 43 additions & 0 deletions date/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package date

import (
"fmt"
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/internal/exit"

"github.com/fxtlabs/date"
)

// Run provides a shell script interface for the date picker component.
func (o Options) Run() error {
picker := basePicker()

picker.prompt = o.Prompt
picker.promptStyle = o.PromptStyle.ToLipgloss()
picker.cursorTextStyle = o.CursorTextStyle.ToLipgloss()
if value, err := date.ParseISO(o.Value); err == nil {
picker.Date = value
}
p := tea.NewProgram(model{
picker: picker,
aborted: false,
header: o.Header,
headerStyle: o.HeaderStyle.ToLipgloss(),
timeout: o.Timeout,
hasTimeout: o.Timeout > 0,
}, tea.WithOutput(os.Stderr))
tm, err := p.Run()
if err != nil {
return fmt.Errorf("failed to run input: %w", err)
}
m := tm.(model)

if m.aborted {
return exit.ErrAborted
}

fmt.Println(m.picker.Value())
return nil
}
70 changes: 70 additions & 0 deletions date/date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Package date provides a shell script interface for picking a date.
//
// The date the user selected will be sent to stdout in ISO-8601 format:
// YYYY-MM-DD.
//
// $ gum date --value 2023-11-28 > date.text
package date

import (
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/gum/timeout"
"github.com/charmbracelet/lipgloss"
)

type model struct {
header string
headerStyle lipgloss.Style
picker *picker
quitting bool
aborted bool
timeout time.Duration
hasTimeout bool
}

func (m model) Init() tea.Cmd {
return tea.Batch(
timeout.Init(m.timeout, nil),
)
}

func (m model) View() string {
if m.quitting {
return ""
}
if m.header != "" {
header := m.headerStyle.Render(m.header)
return lipgloss.JoinVertical(lipgloss.Left, header, m.picker.View())
}

return m.picker.View()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case timeout.TickTimeoutMsg:
if msg.TimeoutValue <= 0 {
m.quitting = true
m.aborted = true
return m, tea.Quit
}
m.timeout = msg.TimeoutValue
return m, timeout.Tick(msg.TimeoutValue, msg.Data)
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc":
m.quitting = true
m.aborted = true
return m, tea.Quit
case "enter":
m.quitting = true
return m, tea.Quit
}
}

var cmd tea.Cmd
m.picker, cmd = m.picker.Update(msg)
return m, cmd
}
18 changes: 18 additions & 0 deletions date/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package date

import (
"time"

"github.com/charmbracelet/gum/style"
)

// Options are the customization options for the date.
type Options struct {
Prompt string `help:"Prompt to display" default:"> " env:"GUM_DATE_PROMPT"`
PromptStyle style.Styles `embed:"" prefix:"prompt." envprefix:"GUM_DATE_PROMPT_"`
CursorTextStyle style.Styles `embed:"" prefix:"cursor." set:"defaultForeground=212" set:"defaultUnderline=true" envprefix:"GUM_DATE_CURSOR_"` //nolint:staticcheck
Value string `help:"Initial value in ISO 8601 format, e.g. 2023-11-28" default:""`
Header string `help:"Header value" default:"" env:"GUM_DATE_HEADER"`
HeaderStyle style.Styles `embed:"" prefix:"header." set:"defaultForeground=240" envprefix:"GUM_DATE_HEADER_"`
Timeout time.Duration `help:"Timeout until input aborts" default:"0" env:"GUM_DATE_TIMEOUT"`
}
118 changes: 118 additions & 0 deletions date/picker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package date

import (
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/fxtlabs/date"
)

type interval int

// Default styles.
var (
weekdayStyle = lipgloss.NewStyle().Faint(true)
defaultCursorTextStyle = lipgloss.NewStyle().Underline(true).Foreground(lipgloss.Color("201"))
)

const (
day interval = 0
month interval = 1
year interval = 2
)

// incr i in direction d; bodge mod-3 indexing.
func (i interval) incr(d direction) interval {
mod := (int(i) + int(d)) % 3
if mod < 0 {
return year
}
return interval(mod)
}

type direction int

const (
forward direction = 1
backward direction = -1
)

// picker implements tea.Model for a date.Date.
type picker struct {
date.Date
focus interval

promptStyle lipgloss.Style
prompt string

cursorTextStyle lipgloss.Style
}

func basePicker() *picker {
return &picker{
Date: date.Today(),
focus: day,
prompt: "> ",
cursorTextStyle: defaultCursorTextStyle,
}
}

func (p *picker) formatDate() string {
raw := p.Date.Format("02 Jan 2006")
parts := strings.Split(raw, " ")
parts[int(p.focus)] = p.cursorTextStyle.Render(parts[int(p.focus)])
return strings.Join(parts, " ") + " " + p.formatWeekday()
}

func (p *picker) formatWeekday() string {
shortName := p.Date.Weekday().String()[:3]
return weekdayStyle.Render(shortName)
}

func (p *picker) incr(d direction) {
switch p.focus {
case day:
p.Date = p.Date.AddDate(0, 0, int(d))
case month:
p.Date = p.Date.AddDate(0, int(d), 0)
case year:
p.Date = p.Date.AddDate(int(d), 0, 0)
}
}

// Init implements tea.Model.
func (p *picker) Init() tea.Cmd {
return nil
}

// Update implements tea.Model.
func (p *picker) Update(msg tea.Msg) (*picker, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
// "up"/"down" increment/decrement the focused component, respectively
case "up", "k":
p.incr(forward)
case "down", "j":
p.incr(backward)

// "left"/"right" cycle the focused component
case "left", "h":
p.focus = p.focus.incr(backward)
case "right", "l":
p.focus = p.focus.incr(forward)
}
}
return p, nil
}

// View implements tea.Model.
func (p *picker) View() string {
return p.promptStyle.Render(p.prompt) + p.formatDate()
}

// Value of p.
func (p *picker) Value() date.Date {
return p.Date
}
4 changes: 4 additions & 0 deletions examples/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ gum choose Unlimited Choice Of Items --no-limit --cursor "* " --cursor-prefix "(
gum confirm "Testing?"
gum confirm "No?" --default=false --affirmative "Okay." --negative "Cancel."

# Date
gum date
gum date --value 2021-10-10

# Filter
gum filter
echo {1..500} | sed 's/ /\n/g' | gum filter
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/charmbracelet/log v0.4.0
github.com/charmbracelet/x/ansi v0.3.2
github.com/charmbracelet/x/term v0.2.0
github.com/fxtlabs/date v0.0.0-20150819233934-d9ab6e2a88a9
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/fxtlabs/date v0.0.0-20150819233934-d9ab6e2a88a9 h1:NERIc41aohgojUAgWCCnN5B8dIXZsBo2UC04LR3tbao=
github.com/fxtlabs/date v0.0.0-20150819233934-d9ab6e2a88a9/go.mod h1:UoIEyXCyEJ1Zu3ejiUOSngl9U5Oe9S+qaNiYiUex2nk=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
Expand Down
12 changes: 12 additions & 0 deletions gum.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/gum/choose"
"github.com/charmbracelet/gum/completion"
"github.com/charmbracelet/gum/confirm"
"github.com/charmbracelet/gum/date"
"github.com/charmbracelet/gum/file"
"github.com/charmbracelet/gum/filter"
"github.com/charmbracelet/gum/format"
Expand Down Expand Up @@ -58,6 +59,17 @@ type Gum struct {
//
Confirm confirm.Options `cmd:"" help:"Ask a user to confirm an action"`

// Date provides an interface for picking a date. Outputs the selected date
// in ISO 8601 format: `YYYY-MM-DD`.
//
// $ gum date
//
// You can specify a start date in ISO 8601 format. Let's pick a date
// starting with the initial value Nov. 28, 2023:
//
// $ gum date --value="2023-11-28"
Date date.Options `cmd:"" help:"Pick a date"`

// File provides an interface to pick a file from a folder (tree).
// The user is provided a file manager-like interface to navigate, to
// select a file.
Expand Down