Skip to content

Commit

Permalink
Add gum date subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasschwab committed Nov 29, 2023
1 parent 01a6651 commit 969153b
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ gum
dist
testdata

# Demos

This comment has been minimized.

Copy link
@lukasschwab

lukasschwab Oct 2, 2024

Author Owner

Revert.

*.tape
*.gif

# Folders
completions/
manpages/
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,24 @@ 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
```

![Demo of ](https://vhs.charm.sh/vhs-2Gkiemx0ALZZBmODcSrg2I.gif)

This comment has been minimized.

Copy link
@lukasschwab

lukasschwab Oct 2, 2024

Author Owner

Duplicate gifs — prefer consistency with rest of file.


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

## Examples

See the [examples](./examples/) directory for more real world use cases.
Expand Down
44 changes: 44 additions & 0 deletions date/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package date

import (
"fmt"
"os"

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

"github.com/fxtlabs/date"

This comment has been minimized.

Copy link
@lukasschwab

lukasschwab Oct 2, 2024

Author Owner

Eliminate this dependency; only used for ISO date parsing.

)

// Run provides a shell script interface for the text input bubble.

This comment has been minimized.

Copy link
@lukasschwab

lukasschwab Oct 2, 2024

Author Owner

Needs cleanup — outdated docstring.

// https://github.com/charmbracelet/bubbles/textinput
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
}
74 changes: 74 additions & 0 deletions date/date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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.WindowSizeMsg:

This comment has been minimized.

Copy link
@lukasschwab

lukasschwab Oct 2, 2024

Author Owner

Cleanup: commented-out code.

// if m.autoWidth {
// m.textinput.Width = msg.Width - lipgloss.Width(m.textinput.Prompt) - 1
// }
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"`
}
135 changes: 135 additions & 0 deletions date/picker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package date

import (
"strings"
"time"

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 {
name := ""
switch p.Date.Weekday() {
case time.Monday:
name = "Mon"
case time.Tuesday:
name = "Tue"
case time.Wednesday:
name = "Wed"
case time.Thursday:
name = "Thu"
case time.Friday:
name = "Fri"
case time.Saturday:
name = "Sat"
case time.Sunday:
name = "Sun"
}
return weekdayStyle.Render(name)
}

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 @@ -25,6 +25,7 @@ require (
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fxtlabs/date v0.0.0-20150819233934-d9ab6e2a88a9 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
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

0 comments on commit 969153b

Please sign in to comment.