diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2026-02-01 22:26:40 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2026-02-01 22:26:40 +0200 |
| commit | fb9b665b79fccd8ef0a13a514dce11f0369e89df (patch) | |
| tree | b5d2a81a9038ed3ee7cdb5074ae17b02631cc4d6 /tui_model.go | |
| parent | 50541ca317e2ae647fc83d25e38825bb5a120296 (diff) | |
| download | weather-fb9b665b79fccd8ef0a13a514dce11f0369e89df.tar.zst | |
Update
Diffstat (limited to '')
| -rw-r--r-- | tui_model.go | 546 |
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 { |
