by roundabout, Monday, 2 February 2026, 11:55:20 (1770033320), pushed by roundabout, Monday, 2 February 2026, 11:55:23 (1770033323)
Author identity: vlad <vlad.muntoiu@gmail.com>
0340e783113a49bda91a607767364ed761859198
main.py
@@ -15,7 +15,8 @@ GNU General Public Licence for more details.
You should have received a copy of the GNU General Public Licence
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import itertools
import locale
import os
import sys
import json
@@ -30,6 +31,10 @@ from gi.repository import Gtk, Gio, GLib, Gdk
module_directory = Path(__file__).resolve().parent
locale.bindtextdomain("panorama-weather", str(module_directory / "locale"))
locale.textdomain("locale")
locale.setlocale(locale.LC_ALL)
_ = locale.gettext
custom_css = """
@@ -37,6 +42,14 @@ custom_css = """
font-weight: 100;
font-size: 5rem;
}
.panorama-weather-table-header {
font-weight: 600;
}
.panorama-weather-table-temperature {
font-size: 1.125em;
}
"""
css_provider = Gtk.CssProvider()
css_provider.load_from_data(custom_css)
@@ -47,6 +60,21 @@ Gtk.StyleContext.add_provider_for_display(
)
def relative_timestamp(datetime: GLib.DateTime) -> str:
now = GLib.DateTime.new_now_local()
seconds = int(now.difference(datetime) / GLib.TIME_SPAN_SECOND)
if seconds < 30:
return _("just now")
if seconds < 240:
return _("{time} seconds ago").format(time=seconds)
if seconds < 7200:
return _("{time} minutes ago").format(time=seconds//60)
if seconds < 345600:
return _("{time} hours ago").format(time=seconds//3600)
return _("{time} days ago").format(time=seconds//86400)
@Gtk.Template(filename=str(module_directory / "panorama-weather-report.ui"))
class WeatherReport(Gtk.Box):
__gtype_name__ = "WeatherReport"
@@ -54,12 +82,89 @@ class WeatherReport(Gtk.Box):
current_details: Gtk.Label = Gtk.Template.Child()
current_temperature: Gtk.Label = Gtk.Template.Child()
current_weather_icon: Gtk.Image = Gtk.Template.Child()
forecast_box: Gtk.ListBox = Gtk.Template.Child()
attribution_label: Gtk.Label = Gtk.Template.Child()
def __init__(self, data, **kwargs):
super().__init__(**kwargs)
self.data = data
self.current_weather_icon.set_from_icon_name(data["icon_name"])
self.current_temperature.set_text(data["temp"])
self.current_temperature.set_text(_("{temp} °{temp_unit}").format(
temp=locale.format_string("%.0f", data["temp_value"]),
temp_unit=data["units"]["temp"]
))
self.current_details.set_text(_("{prec} {prec_unit} in the last hour").format(
prec=locale.format_string("%.2f", data["prec"]),
prec_unit=data["units"]["prec"]
))
self.update_time = GLib.DateTime.new_from_unix_utc(data["time"]).to_local()
self.attribution_label.set_text(_("Retrieved {timestamp}. Source: {attribution}.").format(
timestamp=relative_timestamp(self.update_time),
attribution=data["attribution"]
))
self.time_size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
self.icon_size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
self.temperature_size_group = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
self.header_box = Gtk.Box(spacing=4)
self.time_header = Gtk.Label.new(_("Time"))
self.time_header.add_css_class("panorama-weather-table-header")
self.time_size_group.add_widget(self.time_header)
self.condition_header = Gtk.Box()
self.icon_size_group.add_widget(self.condition_header)
self.temperature_header = Gtk.Label.new(_("Temperature"))
self.temperature_header.add_css_class("panorama-weather-table-header")
self.temperature_size_group.add_widget(self.temperature_header)
self.header_box.append(self.time_header)
self.header_box.append(Gtk.Box(hexpand=True)) # some empty space
self.header_box.append(self.temperature_header)
self.header_box.append(self.condition_header)
self.table_header = Gtk.ListBoxRow(child=self.header_box, activatable=False)
self.update_forecast_table()
def update_forecast_table(self):
self.forecast_box.remove_all()
self.forecast_box.append(self.table_header)
for point in self.data["forecast"]:
box = Gtk.Box(spacing=4)
forecasted_time = GLib.DateTime.new_from_unix_utc(point["time"]).to_local()
time_label = Gtk.Label.new(forecasted_time.format("%H"))
time_label.set_xalign(0)
self.time_size_group.add_widget(time_label)
box.append(time_label)
box.append(Gtk.Box(hexpand=True)) # some empty space
temperature_label = Gtk.Label.new(_("{temp} °{temp_unit}").format(
temp=locale.format_string("%.1f", point["temp_value"]),
temp_unit=self.data["units"]["temp"]
))
temperature_label.set_xalign(1)
temperature_label.add_css_class("panorama-weather-table-temperature")
self.temperature_size_group.add_widget(temperature_label)
box.append(temperature_label)
icon = Gtk.Image.new_from_icon_name(point["icon_name"])
icon.set_icon_size(Gtk.IconSize.LARGE)
icon.set_halign(Gtk.Align.END)
self.icon_size_group.add_widget(icon)
box.append(icon)
row = Gtk.ListBoxRow(child=box)
row.day = GLib.DateTime.new_local(forecasted_time.get_year(), forecasted_time.get_month(), forecasted_time.get_day_of_month(), 0, 0, 0)
self.forecast_box.append(row)
self.forecast_box.set_header_func(self.make_day_headers)
def make_day_headers(self, b: Gtk.ListBoxRow, a: Gtk.ListBoxRow):
if a is not None and hasattr(b, "day") and ((hasattr(a, "day") and not a.day.equal(b.day)) or not hasattr(a, "day")):
b.set_header(Gtk.Label.new(b.day.format("%A, %-d %B")))
else:
b.set_header(None)
@Gtk.Template(filename=str(module_directory / "panorama-weather.ui"))
panorama-weather-report.ui
@@ -1,6 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.96.1 -->
<interface>
<interface domain="panorama-weather">
<!-- interface-name panorama-weather-report.ui -->
<requires lib="gtk" version="4.12"/>
<template class="WeatherReport" parent="GtkBox">
<property name="hexpand">True</property>
@@ -19,6 +20,7 @@
<property name="margin-start">64</property>
<property name="margin-top">32</property>
<property name="spacing">8</property>
<property name="vexpand">True</property>
<child>
<object class="GtkImage" id="current_weather_icon">
<property name="icon-name">image-missing</property>
@@ -28,6 +30,7 @@
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">center</property>
<child>
<object class="GtkLabel" id="current_temperature">
<property name="css-classes">panorama-weather-current-temperature</property>
@@ -43,6 +46,17 @@
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="attribution_label">
<property name="css-classes">panorama-weather-attribution</property>
<property name="margin-bottom">32</property>
<property name="margin-end">32</property>
<property name="margin-start">32</property>
<property name="margin-top">16</property>
<property name="wrap">True</property>
<property name="xalign">0.0</property>
</object>
</child>
</object>
</child>
<child type="tab">
@@ -50,6 +64,28 @@
<property name="label" translatable="yes">Current conditions</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<child>
<object class="GtkViewport">
<child>
<object class="GtkListBox" id="forecast_box">
<property name="margin-bottom">16</property>
<property name="margin-end">16</property>
<property name="margin-start">16</property>
<property name="margin-top">16</property>
<property name="selection-mode">none</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="label" translatable="yes">Forecast</property>
</object>
</child>
</object>
</child>
</template>
panorama-weather.cmb
@@ -2,6 +2,6 @@
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
<!-- Created with Cambalache 0.96.1 -->
<cambalache-project version="0.96.0" target_tk="gtk-4.0">
<ui template-class="WeatherWindow" filename="panorama-weather.ui" sha256="d8b224fba93b16808f27028b726f98dc737b36d4e3a0891a7231334704e7ce8e"/>
<ui template-class="WeatherReport" filename="panorama-weather-report.ui" sha256="92ce3f49ea7c669613dde83773bac584b7ee74f2efd1953dde6c40a8d4c2ff72"/>
<ui template-class="WeatherWindow" filename="panorama-weather.ui" sha256="1a7095d7d0e95cb9909692a4b40518a9ccacae22f3348c4b3721a18a381a41a4"/>
<ui template-class="WeatherReport" filename="panorama-weather-report.ui" sha256="21bc4b4df77f801a0c7df9928f8507e68142433ba2cd2971e14d87981749f05c"/>
</cambalache-project>
panorama-weather.ui
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.96.1 -->
<interface>
<interface domain="panorama-weather">
<!-- interface-name panorama-weather.ui -->
<requires lib="gio" version="2.0"/>
<requires lib="gtk" version="4.12"/>
@@ -92,7 +92,7 @@
<object class="GtkStackPage">
<property name="child">
<object class="GtkLabel">
<property name="label" translatable="yes">Welcome </property>
<property name="label" translatable="yes">Select a city</property>
<property name="wrap">True</property>
</object>
</property>