Skip to content

Commit a0e7d47

Browse files
committed
feat(NixOS): add OS update interface for NixOS
1 parent 2bb2765 commit a0e7d47

File tree

6 files changed

+315
-2
lines changed

6 files changed

+315
-2
lines changed

core/platform/os/nixos.gd

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,45 @@
11
extends OSPlatform
22
class_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

516
func _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'.
1345
func 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)

core/ui/card_ui/settings/general_settings_menu.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
[ext_resource type="Theme" uid="uid://de64j20kxm1k1" path="res://assets/themes/card_ui-water-vapor.tres" id="13_2j54j"]
2525
[ext_resource type="Theme" uid="uid://cw7auu2ayqnp8" path="res://assets/themes/card_ui-mountain.tres" id="14_wt0wj"]
2626

27-
[node name="GeneralSettings" type="ScrollContainer"]
27+
[node name="GeneralSettings" type="ScrollContainer" groups=["menu", "settings_general_menu"]]
2828
anchors_preset = 15
2929
anchor_right = 1.0
3030
anchor_bottom = 1.0

core/ui/card_ui/settings/settings_menu.tscn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ vertical = true
5555

5656
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_a60ci"]
5757

58-
[node name="SettingsMenu" type="Control"]
58+
[node name="SettingsMenu" type="Control" groups=["menu", "settings_menu"]]
5959
layout_mode = 3
6060
anchors_preset = 15
6161
anchor_right = 1.0

project.godot

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ enabled=PackedStringArray("res://addons/gut/plugin.cfg")
5252

5353
import/blender/enabled=false
5454

55+
[global_group]
56+
57+
menu=""
58+
main=""
59+
5560
[input]
5661

5762
ui_accept={

rootfs/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ install: ## Install OpenGamepadUI (default: ~/.local)
4040
$(PREFIX)/share/polkit-1/actions/org.shadowblip.manage_input.policy
4141
install -Dm644 usr/share/polkit-1/actions/org.shadowblip.setcap.policy \
4242
$(PREFIX)/share/polkit-1/actions/org.shadowblip.setcap.policy
43+
install -Dm644 usr/share/polkit-1/actions/org.shadowblip.nixos_updater.policy \
44+
$(PREFIX)/share/polkit-1/actions/org.shadowblip.nixos_updater.policy
4345
install -Dm644 usr/lib/systemd/user/systemd-sysext-updater.service \
4446
$(PREFIX)/lib/systemd/user/systemd-sysext-updater.service
4547
install -Dm644 usr/lib/systemd/user/ogui-overlay-mode.service \
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE policyconfig PUBLIC
3+
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
4+
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
5+
<policyconfig>
6+
7+
<vendor>OpenGamepadUI NixOS Updater</vendor>
8+
<vendor_url>http://www.github.com/shadowblip</vendor_url>
9+
10+
<action id="org.shadowblip.pkexec.nixos_updater">
11+
<description>Update NixOS</description>
12+
<icon_name>package-x-generic</icon_name>
13+
<defaults>
14+
<allow_any>yes</allow_any>
15+
<allow_inactive>yes</allow_inactive>
16+
<allow_active>yes</allow_active>
17+
</defaults>
18+
<annotate key="org.freedesktop.policykit.exec.path">/run/current-system/sw/bin/os-updater</annotate>
19+
</action>
20+
21+
</policyconfig>

0 commit comments

Comments
 (0)