From fb9b665b79fccd8ef0a13a514dce11f0369e89df Mon Sep 17 00:00:00 2001 From: Petri Hienonen Date: Sun, 1 Feb 2026 22:26:40 +0200 Subject: Update --- tui_model.go | 550 +++++++++++++++++++++++------------------------------------ 1 file changed, 212 insertions(+), 338 deletions(-) (limited to 'tui_model.go') 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) - - bars = append(bars, bar) - times = append(times, timeLabel) - temps = append(temps, tempLabel) + tempRow = append(tempRow, + lipgloss.NewStyle(). + Foreground(m.getTempColor(temp)). + Render(fmt.Sprintf("%s%s", tempLabel, icon)), + ) - // Add precipitation indicator and weather icon - if i < len(bars) { - bars[i] = bars[i] + " " + - lipgloss.NewStyle().Foreground(lipgloss.Color("69")).Render(precipChar) + - " " + icon + // Create bar - simple vertical representation + bar := m.createTemperatureBar(temp, minTemp, maxTemp, barHeight) + barRow = append(barRow, bar) + + // 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) - - 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]++ - } - } - - // 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) + // Calculate bar position (0 to height-1) + pos := int(((temp - minTemp) / tempRange) * float64(height-1)) + + // 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 += " " } - - 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 { -- cgit v1.3-1-g0d28