11extends OSPlatform
22class_name PlatformNixOS
33
4+ const UPDATER_CMD : String = "os-updater"
5+
6+ var settings_manager := load ("res://core/global/settings_manager.tres" ) as SettingsManager
7+ var notification_manager := load ("res://core/global/notification_manager.tres" ) as NotificationManager
8+ var sections_label_scene := load ("res://core/ui/components/section_label.tscn" ) as PackedScene
9+ var button_scene := load ("res://core/ui/components/card_button_setting.tscn" ) as PackedScene
10+ var toggle_scene := load ("res://core/ui/components/toggle.tscn" ) as PackedScene
11+
12+ var update_available := false
13+ var update_installed := false
14+
415
516func _init () -> void :
617 logger .set_name ("PlatformNixOS" )
718 logger .set_level (Log .LEVEL .INFO )
819 logger .info ("Detected NixOS platform" )
920
1021
22+ ## Ready will be called after the scene tree has initialized.
23+ func ready (root : Window ) -> void :
24+ logger .info ("READY" )
25+
26+ # Wait for the scene tree to be ready
27+ await root .get_tree ().process_frame
28+
29+ var main := root .get_tree ().get_first_node_in_group ("main" ) as Node
30+ if not main :
31+ logger .warn ("Unable to find main scene" )
32+ return
33+
34+ # Remove the existing updater
35+ _remove_updater (main )
36+
37+ # Add the update interface if the os update script exists
38+ if _has_updater ():
39+ _add_updater (main )
40+
41+
42+
1143## NixOS typically cannot execute regular binaries, so downloaded binaries will
1244## be run with 'steam-run'.
1345func get_binary_compatibility_cmd (cmd : String , args : PackedStringArray ) -> Array [String ]:
@@ -21,3 +53,256 @@ func get_binary_compatibility_cmd(cmd: String, args: PackedStringArray) -> Array
2153 command .append_array (args )
2254
2355 return command
56+
57+
58+ # Returns true if the OS updater script is installed on the system
59+ func _has_updater () -> bool :
60+ return OS .execute ("which" , [UPDATER_CMD ]) == OK
61+
62+
63+ # Removes the update buttons/toggles in the general settings menu
64+ func _remove_updater (root : Node ) -> void :
65+ # Find the general settings menu in the scene tree
66+ var general_settings_menu := root .get_tree ().get_first_node_in_group ("settings_general_menu" )
67+ if not general_settings_menu :
68+ logger .warn ("Unable to find general settings menu" )
69+ return
70+
71+ var updates_label := general_settings_menu .auto_update_toggle .get_parent ().get_node ("UpdatesLabel" ) as Node
72+ if updates_label :
73+ _remove_node (updates_label )
74+
75+ # Remove UI elements that we will replace
76+ var nodes_to_remove : Array [Node ] = [
77+ general_settings_menu .updater ,
78+ general_settings_menu .update_timer ,
79+ general_settings_menu .auto_update_toggle ,
80+ general_settings_menu .check_update_button ,
81+ general_settings_menu .update_button ,
82+ ]
83+ for node in nodes_to_remove :
84+ _remove_node (node )
85+ general_settings_menu .updater = null
86+ general_settings_menu .update_timer = null
87+ general_settings_menu .auto_update_toggle = null
88+ general_settings_menu .check_update_button = null
89+ general_settings_menu .update_button = null
90+
91+
92+ # Adds the NixOS-specific update buttons/toggles to the general settings menu
93+ func _add_updater (root : Node ) -> void :
94+ # Find the general settings menu in the scene tree
95+ var general_settings_menu := root .get_tree ().get_first_node_in_group ("settings_general_menu" )
96+ if not general_settings_menu :
97+ logger .warn ("Unable to find general settings menu" )
98+ return
99+
100+ # Get the container that will have the updater elements
101+ var container := general_settings_menu .lang_dropdown .get_parent () as Container
102+
103+ # Create the updates label
104+ var updates_label := sections_label_scene .instantiate () as Label
105+ updates_label .text = "Updates"
106+ container .add_child (updates_label )
107+ container .move_child (updates_label , 0 )
108+
109+ # Create a timer for auto-updates
110+ var update_timer := Timer .new ()
111+ update_timer .wait_time = 120
112+ general_settings_menu .add_child (update_timer )
113+
114+ # Add the auto-updates toggle
115+ var auto_update_toggle := toggle_scene .instantiate () as Toggle
116+ auto_update_toggle .text = "Automatic Updates"
117+ auto_update_toggle .separator_visible = false
118+ auto_update_toggle .description = "Automatically download and apply updates in the background when they are available"
119+ container .add_child (auto_update_toggle )
120+ container .move_child (auto_update_toggle , 1 )
121+
122+ # Add the check for updates button
123+ var check_update_button := button_scene .instantiate () as CardButtonSetting
124+ check_update_button .text = "Check for updates"
125+ check_update_button .button_text = "Check for updates"
126+ check_update_button .disabled = false
127+ container .add_child (check_update_button )
128+ container .move_child (check_update_button , 2 )
129+
130+ # Add the check for updates button
131+ var update_button := button_scene .instantiate () as CardButtonSetting
132+ update_button .text = "Install Updates"
133+ update_button .button_text = "Update"
134+ update_button .disabled = true
135+ container .add_child (update_button )
136+ container .move_child (update_button , 3 )
137+
138+ # Reset the focus group's initial focus
139+ var focus_group := container .get_node ("FocusGroup" ) as FocusGroup
140+ focus_group .current_focus = auto_update_toggle
141+
142+ # Configure auto updates toggle
143+ var auto_update := settings_manager .get_value ("general.updates" , "auto_update" , false ) as bool
144+ auto_update_toggle .button_pressed = auto_update
145+ var on_auto_update_toggled := func (toggled : bool ):
146+ settings_manager .set_value ("general.updates" , "auto_update" , toggled )
147+ if toggled :
148+ update_timer .start ()
149+ _on_autoupdate (update_button , check_update_button )
150+ else :
151+ update_timer .stop ()
152+ auto_update_toggle .toggled .connect (on_auto_update_toggled )
153+ update_timer .timeout .connect (_on_autoupdate .bind (update_button , check_update_button ))
154+ if auto_update :
155+ update_timer .start ()
156+
157+ # Configure check for updates button
158+ check_update_button .button_up .connect (_on_check_for_updates .bind (update_button , check_update_button ))
159+
160+ # Configure update button
161+ update_button .button_up .connect (_on_update .bind (update_button , check_update_button ))
162+
163+ # TODO: Add branch selector
164+
165+
166+ # Invoked whenever the updater timer times out
167+ func _on_autoupdate (update_button : CardButtonSetting , check_update_button : CardButtonSetting ) -> void :
168+ logger .info ("Automatically checking for updates..." )
169+ update_button .disabled = true
170+ check_update_button .disabled = true
171+ check_update_button .button_text = "Checking..."
172+
173+ var cmd := Command .create (UPDATER_CMD , ["has-update" ])
174+ if cmd .execute () != OK :
175+ logger .warn ("Failed to check for updates" )
176+ update_available = false
177+ _reset_update_buttons (update_button , check_update_button )
178+ return
179+ if await cmd .finished != OK :
180+ logger .warn ("Failed to check for updates:" , cmd .stdout , cmd .stderr )
181+ update_available = false
182+ _reset_update_buttons (update_button , check_update_button )
183+ return
184+
185+ update_available = cmd .stdout .contains ("1" )
186+ _reset_update_buttons (update_button , check_update_button )
187+ if not update_available :
188+ logger .info ("No new updates available" )
189+ return
190+
191+ logger .info ("New update was found. Trying to install it." )
192+ update_button .disabled = true
193+ update_button .button_text = "Updating..."
194+ check_update_button .disabled = true
195+
196+ # Update the flake.lock file
197+ cmd = Command .create (UPDATER_CMD , ["update" ])
198+ if cmd .execute () != OK :
199+ logger .warn ("Failed to update flake.lock" )
200+ update_button .button_text = "Update"
201+ _reset_update_buttons (update_button , check_update_button )
202+ return
203+ if await cmd .finished != OK :
204+ logger .warn ("Failed to update flake.lock:" , cmd .stdout , cmd .stderr )
205+ update_button .button_text = "Update"
206+ _reset_update_buttons (update_button , check_update_button )
207+ return
208+
209+ # Download and apply the upgrade
210+ cmd = Command .create (UPDATER_CMD , ["upgrade" ])
211+ if cmd .execute () != OK :
212+ logger .warn ("Failed to download and apply upgrade" )
213+ update_button .button_text = "Update"
214+ _reset_update_buttons (update_button , check_update_button )
215+ return
216+ if await cmd .finished != OK :
217+ logger .warn ("Failed to download and apply upgrade:" , cmd .stdout , cmd .stderr )
218+ update_button .button_text = "Update"
219+ _reset_update_buttons (update_button , check_update_button )
220+ return
221+
222+ update_button .button_text = "Update"
223+ _reset_update_buttons (update_button , check_update_button )
224+
225+
226+ # Invoked whenever the "Check for updates" button is pressed
227+ func _on_check_for_updates (update_button : CardButtonSetting , check_update_button : CardButtonSetting ) -> void :
228+ update_button .disabled = true
229+ check_update_button .disabled = true
230+ check_update_button .button_text = "Checking..."
231+
232+ var cmd := Command .create (UPDATER_CMD , ["has-update" ])
233+ if cmd .execute () != OK :
234+ logger .warn ("Failed to check for updates" )
235+ update_available = false
236+ _reset_update_buttons (update_button , check_update_button , "Unable to check for updates" )
237+ return
238+ if await cmd .finished != OK :
239+ logger .warn ("Failed to check for updates:" , cmd .stdout , cmd .stderr )
240+ update_available = false
241+ _reset_update_buttons (update_button , check_update_button , "Unable to check for updates" )
242+ return
243+
244+ update_available = cmd .stdout .contains ("1" )
245+ var msg := "Already up to date"
246+ if update_available :
247+ msg = "New update is available"
248+ _reset_update_buttons (update_button , check_update_button , msg )
249+
250+
251+ # Reset the update buttons state and optionally show the given message
252+ func _reset_update_buttons (update_button : CardButtonSetting , check_update_button : CardButtonSetting , msg : String = "" ) -> void :
253+ update_button .disabled = ! update_available
254+ check_update_button .disabled = false
255+ check_update_button .button_text = "Check for updates"
256+ if msg .is_empty ():
257+ return
258+ var notify := Notification .new (msg )
259+ notification_manager .show (notify )
260+
261+
262+ # Invoked when the update button is pressed
263+ func _on_update (update_button : CardButtonSetting , check_update_button : CardButtonSetting ) -> void :
264+ if not update_available :
265+ return
266+ logger .info ("Downloading and applying upgrade" )
267+ update_button .disabled = true
268+ update_button .button_text = "Updating..."
269+ check_update_button .disabled = true
270+
271+ # Update the flake.lock file
272+ var cmd := Command .create (UPDATER_CMD , ["update" ])
273+ if cmd .execute () != OK :
274+ logger .warn ("Failed to update flake.lock" )
275+ update_button .button_text = "Update"
276+ _reset_update_buttons (update_button , check_update_button , "Unable to download update" )
277+ return
278+ if await cmd .finished != OK :
279+ logger .warn ("Failed to update flake.lock:" , cmd .stdout , cmd .stderr )
280+ update_button .button_text = "Update"
281+ _reset_update_buttons (update_button , check_update_button , "Unable to download update" )
282+ return
283+
284+ # Download and apply the upgrade
285+ cmd = Command .create (UPDATER_CMD , ["upgrade" ])
286+ if cmd .execute () != OK :
287+ logger .warn ("Failed to download and apply upgrade" )
288+ update_button .button_text = "Update"
289+ _reset_update_buttons (update_button , check_update_button , "Unable to download update" )
290+ return
291+ if await cmd .finished != OK :
292+ logger .warn ("Failed to download and apply upgrade:" , cmd .stdout , cmd .stderr )
293+ update_button .button_text = "Update"
294+ _reset_update_buttons (update_button , check_update_button , "Unable to download update" )
295+ return
296+
297+ update_available = false
298+ update_button .button_text = "Update"
299+ _reset_update_buttons (update_button , check_update_button , "Upgrade complete. Reboot to finish applying latest update." )
300+
301+
302+ func _remove_node (node : Node ) -> void :
303+ if not node :
304+ return
305+ var parent := node .get_parent ()
306+ parent .remove_child (node )
307+ node .queue_free ()
308+ logger .debug ("Removed node:" , node .name )
0 commit comments