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

Various Features #47

Merged
merged 14 commits into from
Aug 29, 2024
15 changes: 15 additions & 0 deletions clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ func (c *Clock) AddDays(n int) {
c.t = c.t.AddDate(0, 0, n)
}

// AddDays adds n days to the current date and clears the minutes
func (c *Clock) AddHours(n int) {
c.t = time.Date(
c.t.Year(),
c.t.Month(),
c.t.Day(),
c.t.Hour(),
0, // Minutes set to 0
0, // Seconds set to 0
0, // Nanoseconds set to 0
c.t.Location(),
)
c.t = c.t.Add(time.Hour * time.Duration(n))
}

// Get the wrapped time.Time struct
func (c *Clock) Time() time.Time {
return c.t
Expand Down
5 changes: 2 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,15 @@ func LoadConfig(tzConfigs []string) (*Config, error) {
zones := make([]*Zone, len(tzConfigs)+1)

// Setup with Local time zone
now := Now.Time()
localZoneName, _ := now.Zone()
localZoneName, _ := time.Now().Zone()
zones[0] = &Zone{
Name: fmt.Sprintf("(%s) Local", localZoneName),
DbName: localZoneName,
}

// Add zones from TZ_LIST
for i, zoneConf := range tzConfigs {
zone, err := SetupZone(now, zoneConf)
zone, err := SetupZone(time.Now(), zoneConf)
if err != nil {
return nil, err
}
Expand Down
90 changes: 65 additions & 25 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"flag"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"time"

Expand All @@ -33,9 +35,6 @@ const CurrentVersion = "0.7.0"
var (
term = termenv.ColorProfile()
hasDarkBackground = termenv.HasDarkBackground()

// Now is used around tz to share/set the current time.
Now *Clock = NewClock(0)
)

type tickMsg time.Time
Expand All @@ -47,13 +46,38 @@ func tick() tea.Cmd {
})
}

func openURL(url string) error {
var cmd *exec.Cmd

switch runtime.GOOS {
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
case "darwin":
cmd = exec.Command("open", url)
case "linux":
cmd = exec.Command("xdg-open", url)
default:
return fmt.Errorf("unsupported platform")
}

return cmd.Start()
}

func openInTimeAndDateDotCom(t time.Time) error {
utcTime := t.In(time.UTC).Format("20060102T150405")
url := fmt.Sprintf("https://www.timeanddate.com/worldclock/converter.html?iso=%s&p1=1440", utcTime)

return openURL(url)
}

type model struct {
zones []*Zone
now time.Time
hour int
clock Clock
showDates bool
interactive bool
isMilitary bool
watch bool
showHelp bool
}

func (m model) Init() tea.Cmd {
Expand All @@ -66,7 +90,7 @@ func (m model) Init() tea.Cmd {
return tick()
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {

case tea.KeyMsg:
Expand All @@ -75,27 +99,40 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit

case "left", "h":
if m.hour == 0 {
m.hour = 23
Now.AddDays(-1)
} else {
m.hour--
}
m.clock.AddHours(-1)

case "right", "l":
if m.hour > 22 {
m.hour = 0
Now.AddDays(1)
} else {
m.hour++
}
m.clock.AddHours(1)

case "H":
m.clock.AddDays(-1)

case "L":
m.clock.AddDays(1)

case "<":
m.clock.AddDays(-7)

case ">":
m.clock.AddDays(7)

case "o":
openInTimeAndDateDotCom(m.clock.Time())

case "t":
m.clock = *NewClock(0)

case "?":
m.showHelp = !m.showHelp

case "d":
m.showDates = !m.showDates
}

case tickMsg:
m.now = time.Time(msg)
if m.watch {
m.clock = *NewClock(0)
}
return m, tick()
}
return m, nil
Expand All @@ -107,6 +144,7 @@ func main() {
when := flag.Int64("when", 0, "time in seconds since unix epoch")
doSearch := flag.Bool("list", false, "list zones by name")
military := flag.Bool("m", false, "use 24-hour time")
watch := flag.Bool("w", false, "watch live, set time to now every minute")
flag.Parse()

if *showVersion == true {
Expand All @@ -124,25 +162,27 @@ func main() {
os.Exit(0)
}

if *when != 0 {
Now = NewClock(*when)
}
config, err := LoadConfig(flag.Args())
if err != nil {
fmt.Fprintf(os.Stderr, "Config error: %s\n", err)
os.Exit(2)
}
var initialModel = model{
zones: config.Zones,
now: Now.Time(),
hour: Now.Time().Hour(),
clock: *NewClock(0),
showDates: false,
isMilitary: *military,
watch: *watch,
showHelp: false,
}

if *when != 0 {
initialModel.clock = *NewClock(*when)
}

initialModel.interactive = !*exitQuick

p := tea.NewProgram(initialModel)
p := tea.NewProgram(&initialModel)
if err := p.Start(); err != nil {
fmt.Printf("Alas, there's been an error: %v", err)
os.Exit(1)
Expand Down
39 changes: 27 additions & 12 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,27 @@ package main
import (
"strings"
"testing"
"time"

tea "github.com/charmbracelet/bubbletea"
)

func getTimestampWithHour(hour int) int64 {
if hour == -1 {
hour = time.Now().Hour()
}
return time.Date(
time.Now().Year(),
time.Now().Month(),
time.Now().Day(),
hour,
0, // Minutes set to 0
0, // Seconds set to 0
0, // Nanoseconds set to 0
time.Now().Location(),
).Unix()
}

func TestUpdateIncHour(t *testing.T) {
// "l" key -> go right
msg := tea.KeyMsg{
Expand All @@ -45,19 +62,18 @@ func TestUpdateIncHour(t *testing.T) {
for _, test := range tests {
m := model{
zones: DefaultZones,
hour: test.startHour,
clock: *NewClock(getTimestampWithHour(test.startHour)),
}

// Do we enjoy global mutable state?
db := Now.Time().Day()
db := m.clock.Time().Day()
nextState, cmd := m.Update(msg)
da := Now.Time().Day()
da := m.clock.Time().Day()

if cmd != nil {
t.Errorf("Expected nil Cmd, but got %v", cmd)
return
}
h := nextState.(model).hour
h := nextState.(*model).clock.t.Hour()
if h != test.nextHour {
t.Errorf("Expected %d, but got %d", test.nextHour, h)
}
Expand Down Expand Up @@ -88,14 +104,14 @@ func TestUpdateDecHour(t *testing.T) {
for _, test := range tests {
m := model{
zones: DefaultZones,
hour: test.startHour,
clock: *NewClock(getTimestampWithHour(test.startHour)),
}
nextState, cmd := m.Update(msg)
if cmd != nil {
t.Errorf("Expected nil Cmd, but got %v", cmd)
return
}
h := nextState.(model).hour
h := nextState.(*model).clock.t.Hour()
if h != test.nextHour {
t.Errorf("Expected %d, but got %d", test.nextHour, h)
}
Expand All @@ -112,7 +128,7 @@ func TestUpdateQuitMsg(t *testing.T) {

m := model{
zones: DefaultZones,
hour: 10,
clock: *NewClock(getTimestampWithHour(-1)),
}
_, cmd := m.Update(msg)
if cmd == nil {
Expand All @@ -126,13 +142,12 @@ func TestUpdateQuitMsg(t *testing.T) {
func TestMilitaryTime(t *testing.T) {
m := model{
zones: DefaultZones,
hour: 14,
now: Now.Time(),
clock: *NewClock(getTimestampWithHour(-1)),
isMilitary: true,
showDates: true,
}
s := m.View()
if !strings.Contains(s, m.now.Format("15:04")) {
t.Errorf("Expected military time of %s, but got %s", m.now.Format("15:04"), s)
if !strings.Contains(s, m.clock.t.Format("15:04")) {
t.Errorf("Expected military time of %s, but got %s", m.clock.t.Format("15:04"), s)
}
}
30 changes: 19 additions & 11 deletions view.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (m model) View() string {

startHour := 0
if zi > 0 {
startHour = (zone.currentTime().Hour() - m.zones[0].currentTime().Hour()) % 24
startHour = (zone.currentTime(m.clock.t).Hour() - m.zones[0].currentTime(m.clock.t).Hour()) % 24
}

dateChanged := false
Expand All @@ -46,8 +46,8 @@ func (m model) View() string {

out = out.Foreground(term.Color(hourColorCode(hour)))
// Cursor
if m.hour == i-startHour {
out = out.Background(term.Color("#00B67F"))
if m.clock.t.Hour() == i-startHour {
out = out.Background(term.Color(hourColorCode(hour)))
if hasDarkBackground {
out = out.Foreground(term.Color("#262626")).Bold()
} else {
Expand All @@ -72,24 +72,32 @@ func (m model) View() string {

var datetime string
if m.isMilitary {
datetime = zone.ShortMT()
datetime = zone.ShortMT(m.clock.t)
} else {
datetime = zone.ShortDT()
datetime = zone.ShortDT(m.clock.t)
}

zoneHeader := fmt.Sprintf("%s %s %s", zone.ClockEmoji(), normalTextStyle(zone.String()), dateTimeStyle(datetime))
zoneHeader := fmt.Sprintf("%s %-60s %76s", zone.ClockEmoji(m.clock.t), normalTextStyle(zone.String()), dateTimeStyle(datetime))

s += fmt.Sprintf(" %s\n %s\n %s\n", zoneHeader, hours.String(), dates.String())
}

if m.interactive {
s += status()
s += status(m)
}
return s
}

func status() string {
text := " q: quit, d: toggle date"
func status(m model) string {

var text string

if m.showHelp {
text = " q: quit, ?: help, h/l: hours, H/L: days, </>: weeks, d: toggle date, t: now, o: open in web"
} else {
text = " q: quit, ?: help"
}

for {
text += " "
if len(text) > UIWidth {
Expand All @@ -109,8 +117,8 @@ func status() string {
}

func formatDayChange(m *model, z *Zone) string {
zTime := z.currentTime()
if zTime.Hour() > m.now.Hour() {
zTime := z.currentTime(m.clock.t)
if zTime.Hour() > m.clock.t.Hour() {
zTime = zTime.AddDate(0, 0, 1)
}

Expand Down
Loading