summaryrefslogtreecommitdiffstats
path: root/tui_model.go
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2026-02-01 22:26:40 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2026-02-01 22:26:40 +0200
commitfb9b665b79fccd8ef0a13a514dce11f0369e89df (patch)
treeb5d2a81a9038ed3ee7cdb5074ae17b02631cc4d6 /tui_model.go
parent50541ca317e2ae647fc83d25e38825bb5a120296 (diff)
downloadweather-fb9b665b79fccd8ef0a13a514dce11f0369e89df.tar.zst
Update
Diffstat (limited to 'tui_model.go')
-rw-r--r--tui_model.go546
1 files changed, 210 insertions, 336 deletions
diff --git a/tui_model.go b/tui_model.go
index 5416a39..9c29656 100644
--- a/tui_model.go
+++ b/tui_model.go
@@ -1,4 +1,4 @@
-// tui_model.go - Fixed version
+// tui_model.go - Updated to match the reference UI
package main
import (
@@ -179,33 +179,34 @@ func (m Model) View() string {
}
func (m Model) mainView() string {
- var sections []string
-
- // Header with location and current conditions
- sections = append(sections, m.renderHeader())
-
- // Current weather
- sections = append(sections, m.renderCurrentWeather())
-
- // Hourly or daily forecast
- if m.showHourly {
- sections = append(sections, m.renderHourlyForecast())
- } else {
- sections = append(sections, m.renderDailyForecast())
- }
-
- // Weather details
- sections = append(sections, m.renderDetails())
+ // Create a box with border
+ boxStyle := lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(lipgloss.Color("240")).
+ Padding(1, 2).
+ Width(m.width - 4) // Account for padding and border
- // Combine sections with spacing
- return lipgloss.JoinVertical(lipgloss.Left,
- sections...,
+ content := lipgloss.JoinVertical(lipgloss.Left,
+ m.renderHeader(),
+ "",
+ m.renderCurrentWeather(),
+ "",
+ m.renderHourlyForecast(),
)
+
+ return boxStyle.Render(content)
}
func (m Model) renderHeader() string {
current := m.response.Current
+ // Format location
+ locParts := strings.Split(m.location, ",")
+ locationText := m.location
+ if len(locParts) > 1 {
+ locationText = strings.TrimSpace(locParts[0])
+ }
+
// Get weather description
description := "Unknown"
if len(current.Weather) > 0 {
@@ -216,166 +217,133 @@ func (m Model) renderHeader() string {
temp := fmt.Sprintf("%.1f°C", current.Temp)
feelsLike := fmt.Sprintf("%.1f°C", current.FeelsLike)
- title := lipgloss.NewStyle().
- Foreground(lipgloss.Color("15")).
- Bold(true).
- Padding(0, 1).
- Render(fmt.Sprintf("%s • %s", m.location, description))
-
- tempStyle := lipgloss.NewStyle().
- Foreground(lipgloss.Color("39")).
- Bold(true).
- Render(temp)
-
- feelsStyle := lipgloss.NewStyle().
- Foreground(lipgloss.Color("246")).
- Render(fmt.Sprintf("Feels like %s", feelsLike))
-
// Get timezone-aware current time
loc, _ := time.LoadLocation(m.response.Timezone)
currentTime := time.Now().In(loc).Format("15:04 MST")
- timeStyle := lipgloss.NewStyle().
- Foreground(lipgloss.Color("245")).
- Italic(true).
- Render(currentTime)
-
+ // Build header
header := lipgloss.JoinVertical(lipgloss.Left,
- title,
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("15")).
+ Bold(true).
+ Render(fmt.Sprintf("%s • %s", locationText, description)),
"",
- lipgloss.JoinHorizontal(lipgloss.Center,
- tempStyle,
- lipgloss.NewStyle().Width(4).Render(""),
- feelsStyle,
+ lipgloss.JoinHorizontal(lipgloss.Left,
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("39")).
+ Bold(true).
+ Width(10).
+ Render(temp),
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("246")).
+ Render(fmt.Sprintf("Feels like %s", feelsLike)),
),
"",
- timeStyle,
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("245")).
+ Italic(true).
+ Render(currentTime),
)
- return lipgloss.NewStyle().
- Border(lipgloss.RoundedBorder()).
- BorderForeground(lipgloss.Color("240")).
- Padding(1, 2).
- Margin(0, 0, 1, 0).
- Render(header)
+ return header
}
func (m Model) renderCurrentWeather() string {
current := m.response.Current
- // Left column: Basic metrics
- leftColumn := []string{
- m.renderMetric("💧 Humidity", fmt.Sprintf("%d%%", current.Humidity)),
- m.renderMetric("💨 Wind", fmt.Sprintf("%.1f km/h", current.WindSpeed*3.6)),
- m.renderMetric("🧭 Direction", fmt.Sprintf("%d°", current.WindDeg)),
- m.renderMetric("🌀 Pressure", fmt.Sprintf("%d hPa", current.Pressure)),
- }
+ // Calculate sunrise and sunset times
+ loc, _ := time.LoadLocation(m.response.Timezone)
- // Right column: Advanced metrics
- rightColumn := []string{
- m.renderMetric("🌫️ Dew Point", fmt.Sprintf("%.1f°C", current.DewPoint)),
- m.renderMetric("👁️ Visibility", fmt.Sprintf("%d m", current.Visibility)),
- m.renderMetric("☁️ Clouds", fmt.Sprintf("%d%%", current.Clouds)),
- m.renderMetric("☀️ UV Index", fmt.Sprintf("%.1f", current.Uvi)),
- }
+ // Use the actual sunrise/sunset from API response
+ sunriseTime := time.Unix(current.Sunrise, 0).In(loc).Format("15:04")
+ sunsetTime := time.Unix(current.Sunset, 0).In(loc).Format("15:04")
- // Sunrise/Sunset
- sunrise := time.Unix(current.Sunrise, 0).Format("15:04")
- sunset := time.Unix(current.Sunset, 0).Format("15:04")
+ // Left column
+ leftColumn := lipgloss.JoinVertical(lipgloss.Left,
+ m.renderMetricRow("💧 Humidity", fmt.Sprintf("%d%%", current.Humidity)),
+ m.renderMetricRow("💨 Wind", fmt.Sprintf("%.1f km/h", current.WindSpeed*3.6)),
+ m.renderMetricRow("🧭 Direction", fmt.Sprintf("%d°", current.WindDeg)),
+ m.renderMetricRow("🌀 Pressure", fmt.Sprintf("%.0f hPa", float64(current.Pressure))),
+ )
- sunInfo := lipgloss.JoinHorizontal(lipgloss.Center,
- lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Render("☀️ "+sunrise),
- lipgloss.NewStyle().Width(10).Render(""),
- lipgloss.NewStyle().Foreground(lipgloss.Color("69")).Render("🌙 "+sunset),
+ // Right column
+ rightColumn := lipgloss.JoinVertical(lipgloss.Left,
+ m.renderMetricRow("🌫️ Dew Point", fmt.Sprintf("%.1f°C", current.DewPoint)),
+ m.renderMetricRow("👁️ Visibility", fmt.Sprintf("%d m", current.Visibility)),
+ m.renderMetricRow("☁️ Clouds", fmt.Sprintf("%d%%", current.Clouds)),
+ m.renderMetricRow("☀️ UV Index", fmt.Sprintf("%.1f", current.Uvi)),
+ )
+
+ // Sunrise/Sunset
+ sunRow := lipgloss.JoinHorizontal(lipgloss.Center,
+ lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Render("☀️ "+sunriseTime),
+ lipgloss.NewStyle().Width(20).Render(""),
+ lipgloss.NewStyle().Foreground(lipgloss.Color("69")).Render("🌙 "+sunsetTime),
)
columns := lipgloss.JoinHorizontal(lipgloss.Top,
- lipgloss.JoinVertical(lipgloss.Left, leftColumn...),
+ leftColumn,
lipgloss.NewStyle().Width(10).Render(""),
- lipgloss.JoinVertical(lipgloss.Left, rightColumn...),
+ rightColumn,
)
- return lipgloss.NewStyle().
- Padding(0, 1).
- Margin(0, 0, 1, 0).
- Render(lipgloss.JoinVertical(lipgloss.Left,
- columns,
- "",
- sunInfo,
- ))
+ return lipgloss.JoinVertical(lipgloss.Left,
+ columns,
+ "",
+ sunRow,
+ )
}
func (m Model) renderHourlyForecast() string {
- // Take next 24 hours or available data
- hours := min(24, len(m.response.Hourly))
+ // Take next 8 hours for better display
+ hours := min(8, len(m.response.Hourly))
if hours == 0 {
return ""
}
- // Find min/max for scaling
+ // Find min/max temperatures for the range display
minTemp := math.MaxFloat64
maxTemp := -math.MaxFloat64
- maxPrecip := 0.0
for i := 0; i < hours; i++ {
- hour := m.response.Hourly[i]
- if hour.Temp < minTemp {
- minTemp = hour.Temp
+ temp := m.response.Hourly[i].Temp
+ if temp < minTemp {
+ minTemp = temp
}
- if hour.Temp > maxTemp {
- maxTemp = hour.Temp
- }
- if hour.Rain != nil && hour.Rain.OneH > maxPrecip {
- maxPrecip = hour.Rain.OneH
+ if temp > maxTemp {
+ maxTemp = temp
}
}
- tempRange := maxTemp - minTemp
- if tempRange == 0 {
- tempRange = 1
- }
+ // Create title with temperature range
+ title := lipgloss.JoinHorizontal(lipgloss.Left,
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("15")).
+ Bold(true).
+ Render("Hourly Forecast"),
+ lipgloss.NewStyle().Width(m.width/2).Render(""),
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("246")).
+ Render(fmt.Sprintf("%.0f/%.0f°C", maxTemp, minTemp)),
+ )
- var bars []string
- var times []string
- var temps []string
+ // Create temperature row (above bars)
+ var tempRow []string
+ var barRow []string
+ var timeRow []string
- barHeight := 10
+ // Set bar height
+ barHeight := 6
for i := 0; i < hours; i++ {
hour := m.response.Hourly[i]
+ temp := hour.Temp
- // Time label
- t := time.Unix(hour.Dt, 0)
- timeLabel := t.Format("15")
- if i == 0 {
- timeLabel = "Now"
- }
-
- // Temperature bar
- tempHeight := int(((hour.Temp - minTemp) / tempRange) * float64(barHeight))
- bar := ""
- for j := 0; j < barHeight; j++ {
- if j < tempHeight {
- // Gradient color based on temperature
- color := m.getTempColor(hour.Temp)
- bar += lipgloss.NewStyle().Foreground(color).Render("▊")
- } else {
- bar += " "
- }
- }
-
- // Precipitation indicator
- precip := 0.0
- if hour.Rain != nil {
- precip = hour.Rain.OneH
- }
-
- precipChar := " "
- if precip > 0.1 {
- precipChar = "•"
- if precip > 2.5 {
- precipChar = "●"
- }
+ // Temperature label (subscript formatting for negative)
+ tempLabel := fmt.Sprintf("%.0f", math.Abs(temp))
+ if temp < 0 {
+ // Use subscript for negative sign (approximation)
+ tempLabel = "₋" + tempLabel
}
// Weather icon
@@ -384,190 +352,94 @@ func (m Model) renderHourlyForecast() string {
icon = m.getWeatherIcon(hour.Weather[0].Icon)
}
- // Temperature label
- tempLabel := fmt.Sprintf("%.0f°", hour.Temp)
+ tempRow = append(tempRow,
+ lipgloss.NewStyle().
+ Foreground(m.getTempColor(temp)).
+ Render(fmt.Sprintf("%s%s", tempLabel, icon)),
+ )
- bars = append(bars, bar)
- times = append(times, timeLabel)
- temps = append(temps, tempLabel)
+ // Create bar - simple vertical representation
+ bar := m.createTemperatureBar(temp, minTemp, maxTemp, barHeight)
+ barRow = append(barRow, bar)
- // Add precipitation indicator and weather icon
- if i < len(bars) {
- bars[i] = bars[i] + " " +
- lipgloss.NewStyle().Foreground(lipgloss.Color("69")).Render(precipChar) +
- " " + icon
+ // Time label (with special formatting for hour 0)
+ time := time.Unix(hour.Dt, 0).Format("15")
+ if time == "00" {
+ time = "⁰⁰"
+ } else if time == "03" {
+ time = "⁰³"
+ } else if time == "06" {
+ time = "⁰⁜"
+ } else if time == "09" {
+ time = "⁰⁚"
+ } else if time == "12" {
+ time = "š²"
+ } else if time == "15" {
+ time = "š⁾"
+ } else if time == "18" {
+ time = "š⁸"
+ } else if time == "21" {
+ time = "²š"
}
- }
- // Create the chart
- chart := lipgloss.JoinVertical(lipgloss.Right,
- lipgloss.JoinHorizontal(lipgloss.Bottom, temps...),
- lipgloss.JoinHorizontal(lipgloss.Top, bars...),
- lipgloss.JoinHorizontal(lipgloss.Top, times...),
- )
+ timeRow = append(timeRow,
+ lipgloss.NewStyle().
+ Foreground(lipgloss.Color("245")).
+ Render(fmt.Sprintf("%s˙⁰⁰", time)),
+ )
+ }
- title := lipgloss.NewStyle().
- Foreground(lipgloss.Color("15")).
- Bold(true).
- Render("Hourly Forecast")
+ // Build the chart
+ tempRowStr := lipgloss.JoinHorizontal(lipgloss.Center, tempRow...)
+ barRowStr := lipgloss.JoinHorizontal(lipgloss.Center, barRow...)
+ timeRowStr := lipgloss.JoinHorizontal(lipgloss.Center, timeRow...)
- rangeLabel := lipgloss.NewStyle().
- Foreground(lipgloss.Color("246")).
- Render(fmt.Sprintf("%.0f/%.0f°C", maxTemp, minTemp))
+ chart := lipgloss.JoinVertical(lipgloss.Left,
+ "",
+ tempRowStr,
+ "",
+ barRowStr,
+ "",
+ timeRowStr,
+ )
- header := lipgloss.JoinHorizontal(lipgloss.Left,
+ return lipgloss.JoinVertical(lipgloss.Left,
title,
- lipgloss.NewStyle().Width(m.width/2).Render(""),
- rangeLabel,
+ "",
+ chart,
)
-
- return lipgloss.NewStyle().
- Padding(0, 1).
- Margin(0, 0, 1, 0).
- Render(lipgloss.JoinVertical(lipgloss.Left,
- header,
- "",
- chart,
- ))
}
-func (m Model) renderDailyForecast() string {
- // Group by day
- daily := make(map[string][]Current)
- loc, _ := time.LoadLocation(m.response.Timezone)
-
- for _, hour := range m.response.Hourly {
- t := time.Unix(hour.Dt, 0).In(loc)
- day := t.Format("2006-01-02")
- daily[day] = append(daily[day], hour)
- }
-
- // Sort days
- var days []string
- for day := range daily {
- days = append(days, day)
- }
-
- if len(days) == 0 {
- return ""
+func (m Model) createTemperatureBar(temp, minTemp, maxTemp float64, height int) string {
+ // Normalize temperature to bar height
+ tempRange := maxTemp - minTemp
+ if tempRange == 0 {
+ tempRange = 1
}
- var rows []string
-
- for i, day := range days {
- if i >= m.days {
- break
- }
-
- hours := daily[day]
- if len(hours) == 0 {
- continue
- }
-
- // Calculate day statistics
- minTemp := math.MaxFloat64
- maxTemp := -math.MaxFloat64
- var totalPrecip float64
- weatherCount := make(map[int]int)
+ // Calculate bar position (0 to height-1)
+ pos := int(((temp - minTemp) / tempRange) * float64(height-1))
- for _, hour := range hours {
- if hour.Temp < minTemp {
- minTemp = hour.Temp
- }
- if hour.Temp > maxTemp {
- maxTemp = hour.Temp
- }
- if hour.Rain != nil {
- totalPrecip += hour.Rain.OneH
- }
- if len(hour.Weather) > 0 {
- weatherCount[hour.Weather[0].ID]++
- }
+ // Create the bar
+ bar := ""
+ for i := 0; i < height; i++ {
+ if i == pos {
+ // Use block character at the temperature position
+ bar += "▊"
+ } else if (temp > 0 && i < pos) || (temp < 0 && i > pos) {
+ // Use lighter block for the bar
+ bar += "▊"
+ } else {
+ bar += " "
}
-
- // Find most common weather
- commonWeather := Weather{ID: 800, Main: "Clear", Description: "clear sky", Icon: "01d"}
- maxCount := 0
-
- // Use the global FmiToOwm map from weather_mapper.go
- for _, weather := range FmiToOwm {
- count, exists := weatherCount[weather.ID]
- if exists && count > maxCount {
- maxCount = count
- commonWeather = weather
- // Set day/night icon based on time
- t, _ := time.Parse("2006-01-02", day)
- if t.Hour() < 18 && t.Hour() > 6 {
- commonWeather.Icon = commonWeather.Icon + "d"
- } else {
- commonWeather.Icon = commonWeather.Icon + "n"
- }
- }
- }
-
- // Format day
- t, _ := time.Parse("2006-01-02", day)
- dayLabel := t.Format("Mon 02")
- if i == 0 {
- dayLabel = "Today"
- } else if i == 1 {
- dayLabel = "Tomorrow"
- }
-
- // Weather icon
- icon := m.getWeatherIcon(commonWeather.Icon)
-
- // Temperature range
- tempRange := fmt.Sprintf("%.0f/%.0f°C", maxTemp, minTemp)
-
- // Precipitation
- precip := ""
- if totalPrecip > 0 {
- precip = fmt.Sprintf("💧 %.1f mm", totalPrecip)
- }
-
- row := lipgloss.JoinHorizontal(lipgloss.Left,
- lipgloss.NewStyle().Width(12).Render(dayLabel),
- lipgloss.NewStyle().Width(3).Render(icon),
- lipgloss.NewStyle().Width(15).Render(commonWeather.Description),
- lipgloss.NewStyle().Width(12).Render(tempRange),
- lipgloss.NewStyle().Width(12).Render(precip),
- )
-
- rows = append(rows, row)
}
- title := lipgloss.NewStyle().
- Foreground(lipgloss.Color("15")).
- Bold(true).
- Render("Daily Forecast")
-
return lipgloss.NewStyle().
- Padding(0, 1).
- Margin(0, 0, 1, 0).
- Render(lipgloss.JoinVertical(lipgloss.Left,
- title,
- "",
- lipgloss.JoinVertical(lipgloss.Left, rows...),
- ))
+ Foreground(m.getTempColor(temp)).
+ Render(bar)
}
-func (m Model) renderDetails() string {
- current := m.response.Current
-
- details := []string{
- m.renderDetailRow("Wind Gust", fmt.Sprintf("%.1f km/h", current.WindGust*3.6)),
- m.renderDetailRow("Dew Point", fmt.Sprintf("%.1f°C", current.DewPoint)),
- m.renderDetailRow("Humidity", fmt.Sprintf("%d%%", current.Humidity)),
- m.renderDetailRow("Pressure", fmt.Sprintf("%d hPa", current.Pressure)),
- }
-
- return lipgloss.NewStyle().
- Padding(0, 1).
- Render(lipgloss.JoinVertical(lipgloss.Left, details...))
-}
-
-func (m Model) renderMetric(label, value string) string {
+func (m Model) renderMetricRow(label, value string) string {
return lipgloss.JoinHorizontal(lipgloss.Left,
lipgloss.NewStyle().
Foreground(lipgloss.Color("246")).
@@ -575,61 +447,63 @@ func (m Model) renderMetric(label, value string) string {
Render(label+":"),
lipgloss.NewStyle().
Foreground(lipgloss.Color("15")).
- Bold(true).
+ Width(15).
Render(value),
)
}
-func (m Model) renderDetailRow(label, value string) string {
- return lipgloss.NewStyle().
- Foreground(lipgloss.Color("240")).
- Render(fmt.Sprintf("%s: %s", label, value))
-}
-
func (m Model) getTempColor(temp float64) lipgloss.Color {
- // Color gradient from blue (cold) to red (hot)
- if temp < -10 {
- return lipgloss.Color("27") // Dark blue
+ // Color gradient based on temperature
+ if temp < -15 {
+ return lipgloss.Color("27") // Deep blue
+ } else if temp < -10 {
+ return lipgloss.Color("39") // Blue
+ } else if temp < -5 {
+ return lipgloss.Color("45") // Light blue
} else if temp < 0 {
- return lipgloss.Color("39") // Light blue
- } else if temp < 10 {
+ return lipgloss.Color("51") // Cyan
+ } else if temp < 5 {
return lipgloss.Color("50") // Green
+ } else if temp < 10 {
+ return lipgloss.Color("190") // Yellow-green
+ } else if temp < 15 {
+ return lipgloss.Color("220") // Yellow
} else if temp < 20 {
- return lipgloss.Color("190") // Yellow
- } else if temp < 30 {
return lipgloss.Color("208") // Orange
+ } else if temp < 25 {
+ return lipgloss.Color("202") // Red-orange
} else {
return lipgloss.Color("196") // Red
}
}
func (m Model) getWeatherIcon(icon string) string {
- // Map OpenWeatherMap icons to Unicode/emoji
+ // Map OpenWeatherMap icons to Nerd Font icons (approximation)
iconMap := map[string]string{
- "01d": "☀️", // clear sky day
- "01n": "🌙", // clear sky night
- "02d": "⛅", // few clouds day
- "02n": "☁️", // few clouds night
- "03d": "☁️", // scattered clouds
- "03n": "☁️",
- "04d": "☁️", // broken clouds
- "04n": "☁️",
- "09d": "🌧️", // shower rain
- "09n": "🌧️",
- "10d": "🌦️", // rain day
- "10n": "🌧️", // rain night
- "11d": "⛈️", // thunderstorm day
- "11n": "⛈️", // thunderstorm night
- "13d": "❄️", // snow
- "13n": "❄️",
- "50d": "🌫️", // mist
- "50n": "🌫️",
+ "01d": "", // clear sky day (sun)
+ "01n": "", // clear sky night (moon)
+ "02d": "", // few clouds day
+ "02n": "", // few clouds night
+ "03d": "", // scattered clouds
+ "03n": "",
+ "04d": "", // broken clouds
+ "04n": "",
+ "09d": "", // shower rain
+ "09n": "",
+ "10d": "", // rain day
+ "10n": "", // rain night
+ "11d": "", // thunderstorm day
+ "11n": "", // thunderstorm night
+ "13d": "", // snow
+ "13n": "",
+ "50d": "", // mist
+ "50n": "",
}
- if icon, ok := iconMap[icon]; ok {
- return icon
+ if iconChar, ok := iconMap[icon]; ok {
+ return iconChar
}
- return "🌡️" // default thermometer
+ return "" // default thermometer
}
func (m Model) loadingView() string {