main.py
Python script, ASCII text executable
1
"""
2
Panorama weather: weather app for GNU/Linux desktops.
3
Copyright 2026, roundabout-host.com <vlad@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public Licence as published by
7
the Free Software Foundation, either version 3 of the Licence, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public Licence for more details.
14
15
You should have received a copy of the GNU General Public Licence
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
"""
18
19
import os
20
import sys
21
import json
22
23
import gi
24
25
from pathlib import Path
26
27
gi.require_version("Gtk", "4.0")
28
29
from gi.repository import Gtk, Gio, GLib, Gdk
30
31
32
module_directory = Path(__file__).resolve().parent
33
34
35
custom_css = """
36
.panorama-weather-current-temperature {
37
font-weight: 100;
38
font-size: 5rem;
39
}
40
"""
41
css_provider = Gtk.CssProvider()
42
css_provider.load_from_data(custom_css)
43
Gtk.StyleContext.add_provider_for_display(
44
Gdk.Display.get_default(),
45
css_provider,
46
100
47
)
48
49
50
@Gtk.Template(filename=str(module_directory / "panorama-weather-report.ui"))
51
class WeatherReport(Gtk.Box):
52
__gtype_name__ = "WeatherReport"
53
54
current_details: Gtk.Label = Gtk.Template.Child()
55
current_temperature: Gtk.Label = Gtk.Template.Child()
56
current_weather_icon: Gtk.Image = Gtk.Template.Child()
57
58
def __init__(self, data, **kwargs):
59
super().__init__(**kwargs)
60
61
self.current_weather_icon.set_from_icon_name(data["icon_name"])
62
self.current_temperature.set_text(data["temp"])
63
64
65
@Gtk.Template(filename=str(module_directory / "panorama-weather.ui"))
66
class WeatherWindow(Gtk.Window):
67
__gtype_name__ = "WeatherWindow"
68
69
sidebar_revealer: Gtk.Revealer = Gtk.Template.Child()
70
sidebar: Gtk.Box = Gtk.Template.Child()
71
inner_sidebar: Gtk.Box = Gtk.Template.Child()
72
drag_handle: Gtk.Separator = Gtk.Template.Child()
73
weather_stack: Gtk.Stack = Gtk.Template.Child()
74
75
def sidebar_revealer_change(self, action, state):
76
action.set_state(state)
77
self.sidebar_revealer.set_reveal_child(state.get_boolean())
78
79
def begin_drag(self, gesture, start_x, start_y):
80
x_win, y_win = self.drag_handle.translate_coordinates(self, start_x, start_y)
81
gesture.widget_to_update.start_w = gesture.widget_to_update.get_width()
82
gesture.widget_to_update.start_x = x_win
83
gesture.start_x = start_x
84
85
def update_drag(self, gesture, dx, dy):
86
x_win, y_win = self.drag_handle.translate_coordinates(self, dx + gesture.start_x, 0)
87
gesture.widget_to_update.set_size_request(gesture.widget_to_update.start_w + x_win - gesture.widget_to_update.start_x, -1)
88
89
def set_window_title(self, stack, *args):
90
self.set_title(stack.get_page(stack.get_visible_child()).get_title())
91
92
def __init__(self, **kwargs):
93
super().__init__(**kwargs)
94
self.sidebar_revealer.set_hexpand(False)
95
self.weather_stack.set_hexpand(True)
96
self.weather_stack.set_halign(Gtk.Align.FILL)
97
self.weather_stack.connect("notify::visible-child", self.set_window_title)
98
99
self.action_group = Gio.SimpleActionGroup()
100
self.sidebar_action = Gio.SimpleAction.new_stateful("reveal-sidebar", None, GLib.Variant.new_boolean(True))
101
self.sidebar_action.connect("change-state", self.sidebar_revealer_change)
102
self.action_group.add_action(self.sidebar_action)
103
self.insert_action_group("win", self.action_group)
104
105
drag_controller = Gtk.GestureDrag()
106
drag_controller.widget_to_update = self.sidebar
107
drag_controller.connect("drag-begin", self.begin_drag)
108
drag_controller.connect("drag-update", self.update_drag)
109
self.drag_handle.add_controller(drag_controller)
110
self.drag_handle.set_cursor(Gdk.Cursor.new_from_name("ew-resize"))
111
112
def read_all(self):
113
application: PanoramaWeather = self.get_application()
114
# Read the weather files
115
for file in application.data_directory.iterdir():
116
with open(file, "r") as f:
117
data = json.load(f)
118
self.weather_stack.add_titled(WeatherReport(data), None, data["location"]["name"])
119
120
121
class PanoramaWeather(Gtk.Application):
122
def __init__(self):
123
super().__init__(
124
application_id="com.roundabout_host.roundabout.PanoramaWeather"
125
)
126
self.data_directory = Path(os.environ.get("XDG_DATA_HOME", "~/.local/share")).expanduser() / "weather" / "data" / "locations"
127
128
def do_startup(self):
129
Gtk.Application.do_startup(self)
130
131
def do_shutdown(self):
132
Gtk.Application.do_shutdown(self)
133
134
def do_activate(self):
135
Gtk.Application.do_activate(self)
136
137
window = WeatherWindow()
138
self.add_window(window)
139
window.present()
140
window.read_all()
141
142
143
if __name__ == "__main__":
144
app = PanoramaWeather()
145
app.run(sys.argv)
146