diff --git a/cmd/api/connection.go b/cmd/api/connection.go index 33a7e55..3e0b863 100644 --- a/cmd/api/connection.go +++ b/cmd/api/connection.go @@ -66,7 +66,7 @@ type ConnectionDeparture struct { Vehicle string `json:"vehicle"` Platform string `json:"platform"` //Stops []Stop `json:"stops"` - //VehicleInfo VehicleInfo `json:"vehicleinfo"` + VehicleInfo VehicleInfo `json:"vehicleinfo"` // StationInfo StationInfo `json:"stationinfo"` //PlatformInfo PlatformInfo `json:"platforminfo"` } diff --git a/cmd/api/irail-api.go b/cmd/api/irail-api.go index 515e533..744ca03 100644 --- a/cmd/api/irail-api.go +++ b/cmd/api/irail-api.go @@ -18,7 +18,12 @@ func GetSNCBStationTimeTable(stationName string, time string, arrdep string) ([] if err != nil { return nil, err } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + fmt.Println(fmt.Errorf("couldn't close response body: %v", err)) + } + }(resp.Body) body, err := io.ReadAll(resp.Body) if err != nil { @@ -54,7 +59,12 @@ func GetSNCBStationsJSON() []byte { fmt.Println("Error making request:", err) return nil } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + fmt.Println("Error closing response body:", err) + } + }(resp.Body) // Read the response body body, err := io.ReadAll(resp.Body) diff --git a/cmd/api/timetable_departure_object.go b/cmd/api/timetableDepartureStruct.go similarity index 100% rename from cmd/api/timetable_departure_object.go rename to cmd/api/timetableDepartureStruct.go diff --git a/cmd/spinner.go b/cmd/spinner.go new file mode 100644 index 0000000..3a4b7a8 --- /dev/null +++ b/cmd/spinner.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "time" + + "github.com/briandowns/spinner" +) + +type Spinner struct { + spinner *spinner.Spinner + prefix string + suffix string + sleep time.Duration +} + +func NewSpinner(prefix, suffix string, sleep time.Duration) *Spinner { + s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) + s.Prefix = prefix + s.Suffix = suffix + return &Spinner{ + spinner: s, + prefix: prefix, + suffix: suffix, + sleep: sleep, + } +} + +func (s *Spinner) Start() { + s.spinner.Start() + time.Sleep(s.sleep) +} + +func (s *Spinner) Stop() { + s.spinner.Stop() +} diff --git a/cmd/table.go b/cmd/table.go deleted file mode 100644 index d99ac43..0000000 --- a/cmd/table.go +++ /dev/null @@ -1,119 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/Kaya-Sem/commandtrein/cmd/api" - "os" - "strconv" - - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type model struct { - table table.Model - relativeTime string -} - -func (m model) Init() tea.Cmd { return nil } - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - return m, tea.Quit - case "enter": - return m, tea.Batch( - tea.Printf("detailed info here"), - ) - } - } - - m.table, cmd = m.table.Update(msg) - - // Calculate the relative time for the currently selected row - selectedRow := m.table.SelectedRow() - if selectedRow != nil { - departureTime := selectedRow[0] - relativeTime := CalculateHumanRelativeTime(departureTime) - m.relativeTime = relativeTime - } else { - m.relativeTime = "" - } - - return m, cmd -} - -func (m model) View() string { - // Add the relative time to the view only if there is a selected row - if m.relativeTime != "" { - return baseStyle1.Render(m.table.View()) + "\n\n" + "Departure in: " + m.relativeTime + "\n" - } - return baseStyle1.Render(m.table.View()) + "\n" -} - -func PrintDepartureTable(connections []api.Connection) { - fmt.Println() - columns := []table.Column{ - {Title: "Departure", Width: 10}, - {Title: "Duration", Width: 10}, - {Title: "Arrival", Width: 10}, - {Title: "Track", Width: 5}, - } - - var rows []table.Row - for _, connection := range connections { - departureTime := UnixToHHMM(connection.Departure.Time) - arrivalTime := UnixToHHMM(connection.Arrival.Time) - - // delay is represented as seconds. - delay, _ := strconv.Atoi(connection.Departure.Delay) - delay = delay / 60 - // formattedDelay := strconv.Itoa(delay) - // if delay > 0 { - // formattedDelay = "+" + FormatDelay(delay) - // } - - durationInt, _ := strconv.ParseInt(connection.Duration, 10, 32) - - duration := strconv.FormatInt(durationInt/60, 10) + "m" - - row := table.Row{ - departureTime, - duration, - arrivalTime, - connection.Departure.Platform, - } - - rows = append(rows, row) - } - - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithFocused(true), - table.WithHeight(6), - ) - - s := table.DefaultStyles() - s.Cell.Align(lipgloss.Position(4)) - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - t.SetStyles(s) - - m := model{t, ""} - if _, err := tea.NewProgram(m).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/cmd/tables/connectionTable.go b/cmd/tables/connectionTable.go new file mode 100644 index 0000000..d7d55b7 --- /dev/null +++ b/cmd/tables/connectionTable.go @@ -0,0 +1,123 @@ +package table + +import ( + "fmt" + "github.com/Kaya-Sem/commandtrein/cmd" + "github.com/Kaya-Sem/commandtrein/cmd/api" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type connectionTableModel struct { + table table.Model + relativeTime string + showMessage bool + message string + departures []api.Connection +} + +func (m connectionTableModel) Init() tea.Cmd { return nil } + +func getDetailedConnectionInfo(c api.Connection) string { + return fmt.Sprintf(` +Detailed info: +Destination: %s +Track: %s +Departure Time: %s +Vehicle: %s +`, + c.Departure.Station, + c.Departure.Platform, + cmd.UnixToHHMM(c.Departure.Time), + c.Departure.Vehicle, + ) +} + +func (m connectionTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var teaCmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c", "esc": + return m, tea.Quit + case "enter": + selectedRow := m.table.SelectedRow() + if selectedRow != nil { + selectedIndex := m.table.Cursor() + selectedDeparture := m.departures[selectedIndex] + m.showMessage = true + m.message = getDetailedConnectionInfo(selectedDeparture) + } + return m, tea.Quit + } + } + + m.table, teaCmd = m.table.Update(msg) + + // Calculate the relative time for the currently selected row + selectedRow := m.table.SelectedRow() + if selectedRow != nil { + departureTime := selectedRow[0] + relativeTime := CalculateHumanRelativeTime(departureTime) + m.relativeTime = relativeTime + } else { + m.relativeTime = "" + } + + return m, teaCmd +} + +func (m connectionTableModel) View() string { + if m.showMessage { + // Show the message instead of the tables if the flag is set + return m.message + } + + // Add the relative time to the view only if there is a selected row + if m.relativeTime != "" { + return m.table.View() + "\n\n" + "Departure in: " + m.relativeTime + "\n" + } + return m.table.View() + "\n" +} + +func RenderConnectionTable( + columnItems []table.Column, + rowItems []table.Row, + connections []api.Connection, +) { + fmt.Println() + + columns := columnItems + rows := rowItems + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(tableHeight), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color(BorderColor)). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color(SelectedForeground)). + Background(lipgloss.Color(SelectedBackground)) + t.SetStyles(s) + + m := connectionTableModel{ + table: t, + departures: connections, // Store the departures + } + + if _, err := tea.NewProgram(m).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/cmd/tables/tableUtil.go b/cmd/tables/tableUtil.go new file mode 100644 index 0000000..ec7757a --- /dev/null +++ b/cmd/tables/tableUtil.go @@ -0,0 +1,55 @@ +package table + +import ( + "fmt" + "time" +) + +const ( + BorderColor = "240" // gray + SelectedForeground = "229" // not setting it to yellow will make the text yellow (yellow on purple = white?) + SelectedBackground = "57" // purple + tableHeight = 6 +) + +// CalculateHumanRelativeTime used for calucating human-readable "from now" time. E.g 'in 20 minutes' +func CalculateHumanRelativeTime(departureTime string) string { + now := time.Now() + + depTime, err := time.Parse("15:04", departureTime) + if err != nil { + return "" + } + + // Combine the parsed time with today's date + depDateTime := time.Date(now.Year(), now.Month(), now.Day(), depTime.Hour(), depTime.Minute(), 0, 0, now.Location()) + + // If the departure time is earlier than now, assume it's for the next day + if depDateTime.Before(now) { + depDateTime = depDateTime.Add(24 * time.Hour) + } + + // Calculate the duration between now and the departure time + duration := depDateTime.Sub(now) + + // Handle special cases + if duration < 1*time.Minute { + return "now" + } else if duration < 60*time.Minute { + return fmt.Sprintf("%d min", int(duration.Minutes())) + } else if duration < 120*time.Minute { + minutes := int(duration.Minutes()) % 60 + if minutes == 0 { + return "1 hour" + } + return fmt.Sprintf("1 hour %d min", minutes) + } + + hours := int(duration.Hours()) + minutes := int(duration.Minutes()) % 60 + if minutes == 0 { + return fmt.Sprintf("%d hours", hours) + } + + return fmt.Sprintf("%d hours %d min", hours, minutes) +} diff --git a/cmd/departure-table.go b/cmd/tables/timetableTable.go similarity index 72% rename from cmd/departure-table.go rename to cmd/tables/timetableTable.go index cbe1099..a0cf53d 100644 --- a/cmd/departure-table.go +++ b/cmd/tables/timetableTable.go @@ -1,7 +1,8 @@ -package cmd +package table import ( "fmt" + "github.com/Kaya-Sem/commandtrein/cmd" "github.com/Kaya-Sem/commandtrein/cmd/api" "os" @@ -10,17 +11,7 @@ import ( "github.com/charmbracelet/lipgloss" ) -const ( - BorderColor = "240" // gray - SelectedForeground = "229" // not setting it to yellow will make the text yellow - SelectedBackground = "57" // purple - tableHeight = 6 -) - -var baseStyle1 = lipgloss.NewStyle(). - BorderForeground(lipgloss.Color("9")) - -type tableModel struct { +type timetableTableModel struct { table table.Model relativeTime string showMessage bool @@ -28,7 +19,7 @@ type tableModel struct { departures []api.TimetableDeparture } -func (m tableModel) Init() tea.Cmd { return nil } +func (m timetableTableModel) Init() tea.Cmd { return nil } func getDetailedDepartureInfo(d api.TimetableDeparture) string { return fmt.Sprintf(` @@ -41,14 +32,14 @@ Occupancy: %s `, d.Station, d.Platform, - UnixToHHMM(d.Time), + cmd.UnixToHHMM(d.Time), d.Vehicle, d.Occupancy, ) } -func (m tableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd +func (m timetableTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var teaCmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { @@ -66,37 +57,38 @@ func (m tableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - m.table, cmd = m.table.Update(msg) + m.table, teaCmd = m.table.Update(msg) // Calculate the relative time for the currently selected row selectedRow := m.table.SelectedRow() if selectedRow != nil { departureTime := selectedRow[0] + CalculateHumanRelativeTime(departureTime) relativeTime := CalculateHumanRelativeTime(departureTime) m.relativeTime = relativeTime } else { m.relativeTime = "" } - return m, cmd + return m, teaCmd } var italicStyle = lipgloss.NewStyle().Italic(true) -func (m tableModel) View() string { +func (m timetableTableModel) View() string { if m.showMessage { - // Show the message instead of the table if the flag is set - return baseStyle1.Render(m.message) + // Show the message instead of the tables if the flag is set + return m.message } // Add the relative time to the view only if there is a selected row if m.relativeTime != "" { - return baseStyle1.Render(m.table.View()) + "\n\n" + "Departure in: " + italicStyle.Render(m.relativeTime) + "\n" + return m.table.View() + "\n\n" + "Departure in: " + italicStyle.Render(m.relativeTime) + "\n" } - return baseStyle1.Render(m.table.View()) + "\n" + return m.table.View() + "\n" } -func RenderTable( +func RenderTimetableTable( columnItems []table.Column, rowItems []table.Row, departures []api.TimetableDeparture, @@ -124,7 +116,7 @@ func RenderTable( Background(lipgloss.Color(SelectedBackground)) t.SetStyles(s) - m := tableModel{ + m := timetableTableModel{ table: t, departures: departures, // Store the departures } diff --git a/cmd/util.go b/cmd/util.go index 3ee58b6..2421531 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -7,6 +7,7 @@ import ( "time" ) +// TODO: needed for flag parsing func normalizeTime(time string) string { time = strings.ReplaceAll(time, " ", "") time = strings.ReplaceAll(time, ":", "") @@ -23,60 +24,27 @@ func UnixToHHMM(unixTime string) string { return t.Format("15:04") } -func FormatDelay(minutes int) string { +func FormatDelay(seconds string) string { + minutes, err := strconv.Atoi(seconds) + if err != nil { + return "err" + } + + minutes /= 60 + + // If the delay is 60 minutes or more, convert to hours and minutes if minutes >= 60 { hours := minutes / 60 remainingMinutes := minutes % 60 if remainingMinutes > 0 { - return strconv.Itoa(hours) + "h " + strconv.Itoa(remainingMinutes) + "m" + return "+" + strconv.Itoa(hours) + "h " + strconv.Itoa(remainingMinutes) + "m" } - return strconv.Itoa(hours) + "h" + return "+" + strconv.Itoa(hours) + "h" } - return strconv.Itoa(minutes) + + return "+" + strconv.Itoa(minutes) + "m" } func ShiftArgs(args []string) []string { return args[1:] } - -// CalculateHumanRelativeTime used for calucating human-readable "from now" time. E.g 'in 20 minutes' -func CalculateHumanRelativeTime(departureTime string) string { - now := time.Now() - - depTime, err := time.Parse("15:04", departureTime) - if err != nil { - return "" - } - - // Combine the parsed time with today's date - depDateTime := time.Date(now.Year(), now.Month(), now.Day(), depTime.Hour(), depTime.Minute(), 0, 0, now.Location()) - - // If the departure time is earlier than now, assume it's for the next day - if depDateTime.Before(now) { - depDateTime = depDateTime.Add(24 * time.Hour) - } - - // Calculate the duration between now and the departure time - duration := depDateTime.Sub(now) - - // Handle special cases - if duration < 1*time.Minute { - return "now" - } else if duration < 60*time.Minute { - return fmt.Sprintf("%d min", int(duration.Minutes())) - } else if duration < 120*time.Minute { - minutes := int(duration.Minutes()) % 60 - if minutes == 0 { - return "1 hour" - } - return fmt.Sprintf("1 hour %d min", minutes) - } - - hours := int(duration.Hours()) - minutes := int(duration.Minutes()) % 60 - if minutes == 0 { - return fmt.Sprintf("%d hours", hours) - } - - return fmt.Sprintf("%d hours %d min", hours, minutes) -} diff --git a/go.mod b/go.mod index dde1fa4..45f255f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.9.1 - github.com/cheynewallace/tabby v1.1.1 ) require ( diff --git a/go.sum b/go.sum index 695dd99..0c7b74c 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= -github.com/cheynewallace/tabby v1.1.1 h1:JvUR8waht4Y0S3JF17G6Vhyt+FRhnqVCkk8l4YrOU54= -github.com/cheynewallace/tabby v1.1.1/go.mod h1:Pba/6cUL8uYqvOc9RkyvFbHGrQ9wShyrn6/S/1OYVys= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= diff --git a/main.go b/main.go index 51a0d19..9513ba3 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,15 @@ import ( "fmt" "github.com/Kaya-Sem/commandtrein/cmd" "github.com/Kaya-Sem/commandtrein/cmd/api" + table "github.com/Kaya-Sem/commandtrein/cmd/tables" "os" "time" - "github.com/briandowns/spinner" - "github.com/charmbracelet/bubbles/table" + teaTable "github.com/charmbracelet/bubbles/table" ) func main() { + // TODO: allow flags for time and arrdep args := cmd.ShiftArgs(os.Args) if len(args) == 1 { @@ -27,11 +28,8 @@ func main() { } func handleConnection(stationFrom string, stationTo string) { - s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) - s.Prefix = " " - s.Suffix = " fetching connections..." + s := cmd.NewSpinner(" ", " fetching connections...", 1*time.Second) s.Start() - time.Sleep(1 * time.Second) connectionsJSON, err := api.GetConnections(stationFrom, stationTo, "", "") if err != nil { @@ -43,8 +41,35 @@ func handleConnection(stationFrom string, stationTo string) { panic(err) } + columns := []teaTable.Column{ + {Title: "Departure", Width: 7}, + {Title: "", Width: 2}, + {Title: "Duration", Width: 7}, + {Title: "Arrival", Width: 7}, + {Title: "Track", Width: 10}, + } + + rows := make([]teaTable.Row, len(connections)) + + for i, conn := range connections { + var delay string + if conn.Departure.Delay == "0" { + delay = "" + } else { + delay = cmd.FormatDelay(conn.Departure.Delay) + } + rows[i] = teaTable.Row{ + cmd.UnixToHHMM(conn.Departure.Time), + delay, + conn.Duration, + cmd.UnixToHHMM(conn.Arrival.Time), + conn.Departure.Platform, + } + } + s.Stop() - cmd.PrintDepartureTable(connections) + table.RenderConnectionTable(columns, rows, connections) + } func handleSearch() { @@ -60,11 +85,8 @@ func handleSearch() { } func handleTimetable(stationName string) { - s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) - s.Prefix = " " - s.Suffix = " fetching timetable..." + s := cmd.NewSpinner(" ", " fetching timetable...", 1*time.Second) s.Start() - time.Sleep(1 * time.Second) timetableJSON, err := api.GetSNCBStationTimeTable(stationName, "", "departure") if err != nil { @@ -76,17 +98,25 @@ func handleTimetable(stationName string) { fmt.Printf("failed to parse iRail departures JSON: %v", err) } - columns := []table.Column{ + columns := []teaTable.Column{ {Title: "", Width: 5}, + {Title: "", Width: 4}, {Title: "Destination", Width: 20}, {Title: "Track", Width: 10}, } - rows := make([]table.Row, len(departures)) + rows := make([]teaTable.Row, len(departures)) for i, departure := range departures { - rows[i] = table.Row{ + var delay string + if departure.Delay == "0" { + delay = "" + } else { + delay = cmd.FormatDelay(departure.Delay) + } + rows[i] = teaTable.Row{ cmd.UnixToHHMM(departure.Time), + delay, departure.Station, departure.Platform, } @@ -94,5 +124,5 @@ func handleTimetable(stationName string) { s.Stop() - cmd.RenderTable(columns, rows, departures) + table.RenderTimetableTable(columns, rows, departures) }