depot-linux 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/README.md +56 -0
- data/Rakefile +10 -0
- data/bin/depot +8 -0
- data/bin/depot-gui +14 -0
- data/bin/setup-rubyqt6 +72 -0
- data/fixtures/assets/download.png +0 -0
- data/fixtures/flatpakrefs/org.qbittorrent.qBittorrent.flatpakref +10 -0
- data/fixtures/rpms/Modrinth App-0.13.14-1.x86_64.rpm +0 -0
- data/lib/depot/app_customizer.rb +152 -0
- data/lib/depot/assets.rb +11 -0
- data/lib/depot/backends/app_image.rb +210 -0
- data/lib/depot/backends/archive.rb +263 -0
- data/lib/depot/backends/deb.rb +265 -0
- data/lib/depot/backends/flatpak_ref.rb +123 -0
- data/lib/depot/backends/rpm.rb +280 -0
- data/lib/depot/backends/support.rb +39 -0
- data/lib/depot/cli.rb +344 -0
- data/lib/depot/desktop_entry.rb +37 -0
- data/lib/depot/doctor.rb +59 -0
- data/lib/depot/gui/app.rb +23 -0
- data/lib/depot/gui/drop_panel.rb +80 -0
- data/lib/depot/gui/main_window.rb +1196 -0
- data/lib/depot/inspection.rb +52 -0
- data/lib/depot/inspector.rb +387 -0
- data/lib/depot/installer.rb +54 -0
- data/lib/depot/manifest_store.rb +53 -0
- data/lib/depot/packages/archive.rb +188 -0
- data/lib/depot/packages/deb.rb +262 -0
- data/lib/depot/packages/flatpak_ref.rb +90 -0
- data/lib/depot/packages/rpm.rb +301 -0
- data/lib/depot/paths.rb +57 -0
- data/lib/depot/result.rb +13 -0
- data/lib/depot/sandbox.rb +285 -0
- data/lib/depot/settings.rb +43 -0
- data/lib/depot/source_resolver.rb +36 -0
- data/lib/depot/uninstaller.rb +90 -0
- data/lib/depot/update_downloader.rb +136 -0
- data/lib/depot/updater.rb +230 -0
- data/lib/depot/util.rb +33 -0
- data/lib/depot/version.rb +5 -0
- data/lib/depot.rb +21 -0
- metadata +139 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Depot
|
|
6
|
+
module GUI
|
|
7
|
+
class InstalledAppsTable < RubyQt6::Bando::QTableWidget
|
|
8
|
+
def initialize(owner)
|
|
9
|
+
super(0, 4)
|
|
10
|
+
@owner = owner
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def context_menu_event(event)
|
|
14
|
+
with_event_errors do
|
|
15
|
+
item = item_at(event.pos)
|
|
16
|
+
return unless item
|
|
17
|
+
|
|
18
|
+
set_current_item(item)
|
|
19
|
+
|
|
20
|
+
menu = QMenu.new("", self)
|
|
21
|
+
view = menu.add_action("View info")
|
|
22
|
+
rename = menu.add_action("Change title...")
|
|
23
|
+
icon = menu.add_action("Change icon...")
|
|
24
|
+
reset = menu.add_action("Reset properties")
|
|
25
|
+
sandbox = menu.add_action("Sandbox...")
|
|
26
|
+
menu.add_separator
|
|
27
|
+
reinstall = menu.add_action("Reinstall")
|
|
28
|
+
launch = menu.add_action("Launch")
|
|
29
|
+
uninstall = menu.add_action("Uninstall")
|
|
30
|
+
|
|
31
|
+
case menu.exec(event.global_pos)&.text.to_s
|
|
32
|
+
when view.text.to_s
|
|
33
|
+
@owner.info_selected
|
|
34
|
+
when rename.text.to_s
|
|
35
|
+
@owner.change_title_selected
|
|
36
|
+
when icon.text.to_s
|
|
37
|
+
@owner.change_icon_selected
|
|
38
|
+
when reset.text.to_s
|
|
39
|
+
@owner.reset_selected
|
|
40
|
+
when sandbox.text.to_s
|
|
41
|
+
@owner.sandbox_selected
|
|
42
|
+
when reinstall.text.to_s
|
|
43
|
+
@owner.reinstall_selected
|
|
44
|
+
when launch.text.to_s
|
|
45
|
+
@owner.launch_selected
|
|
46
|
+
when uninstall.text.to_s
|
|
47
|
+
@owner.uninstall_selected
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def with_event_errors
|
|
55
|
+
yield
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
@owner.send(:unexpected_error, e)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class SandboxDialog < RubyQt6::Bando::QDialog
|
|
62
|
+
attr_reader :mode_combo, :profile_combo, :home_combo, :network_check
|
|
63
|
+
|
|
64
|
+
def initialize(parent, manifest, settings)
|
|
65
|
+
super(parent)
|
|
66
|
+
@manifest = Sandbox.normalize(manifest, settings)
|
|
67
|
+
sandbox = @manifest.fetch("sandbox", {})
|
|
68
|
+
|
|
69
|
+
set_window_title("Sandbox")
|
|
70
|
+
set_modal(true)
|
|
71
|
+
resize(430, 260)
|
|
72
|
+
|
|
73
|
+
layout = QVBoxLayout.new
|
|
74
|
+
title = QLabel.new("Sandbox #{manifest.fetch("display_name")}")
|
|
75
|
+
title.set_style_sheet("font-size: 18px; font-weight: 700;")
|
|
76
|
+
layout.add_widget(title)
|
|
77
|
+
|
|
78
|
+
note = QLabel.new(description_text)
|
|
79
|
+
note.set_word_wrap(true)
|
|
80
|
+
note.set_object_name("depotInstallSubtitle")
|
|
81
|
+
layout.add_widget(note)
|
|
82
|
+
|
|
83
|
+
form = QFormLayout.new
|
|
84
|
+
@mode_combo = QComboBox.new
|
|
85
|
+
%w[inherit enabled disabled].each { |value| @mode_combo.add_item(value) }
|
|
86
|
+
@profile_combo = QComboBox.new
|
|
87
|
+
%w[relaxed balanced strict].each { |value| @profile_combo.add_item(value) }
|
|
88
|
+
@home_combo = QComboBox.new
|
|
89
|
+
%w[isolated documents full].each { |value| @home_combo.add_item(value) }
|
|
90
|
+
@network_check = QCheckBox.new("Allow network access")
|
|
91
|
+
|
|
92
|
+
set_combo(@mode_combo, sandbox.fetch("mode", "inherit"))
|
|
93
|
+
set_combo(@profile_combo, sandbox.fetch("profile", "balanced"))
|
|
94
|
+
set_combo(@home_combo, sandbox.fetch("home_access", "documents"))
|
|
95
|
+
@network_check.set_checked(sandbox.fetch("network", true))
|
|
96
|
+
|
|
97
|
+
form.add_row(QString.new("Mode"), @mode_combo)
|
|
98
|
+
form.add_row(QString.new("Profile"), @profile_combo)
|
|
99
|
+
form.add_row(QString.new("Home access"), @home_combo)
|
|
100
|
+
form.add_row(QString.new("Network"), @network_check)
|
|
101
|
+
layout.add_layout(form)
|
|
102
|
+
|
|
103
|
+
buttons = QHBoxLayout.new
|
|
104
|
+
save = QPushButton.new("Save")
|
|
105
|
+
cancel = QPushButton.new("Cancel")
|
|
106
|
+
save.clicked.connect(self, :accept)
|
|
107
|
+
cancel.clicked.connect(self, :reject)
|
|
108
|
+
buttons.add_stretch
|
|
109
|
+
buttons.add_widget(save)
|
|
110
|
+
buttons.add_widget(cancel)
|
|
111
|
+
layout.add_layout(buttons)
|
|
112
|
+
set_layout(layout)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def values
|
|
116
|
+
{
|
|
117
|
+
"mode" => @mode_combo.current_text.to_s,
|
|
118
|
+
"profile" => @profile_combo.current_text.to_s,
|
|
119
|
+
"home_access" => @home_combo.current_text.to_s,
|
|
120
|
+
"network" => @network_check.checked?
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def description_text
|
|
127
|
+
return "Flatpak already manages this app's sandbox. Depot shows that state here, but Bubblewrap settings do not apply." if @manifest["backend"] == "flatpak"
|
|
128
|
+
|
|
129
|
+
"Bubblewrap runs this app through a generated Depot launcher. If Bubblewrap is missing, Depot falls back to the normal launcher so the app still opens."
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def set_combo(combo, value)
|
|
133
|
+
index = combo.find_text(value)
|
|
134
|
+
combo.set_current_index(index) if index && index >= 0
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class MainWindow < RubyQt6::Bando::QMainWindow
|
|
139
|
+
q_object do
|
|
140
|
+
slot "browse_file()"
|
|
141
|
+
slot "inspect_current()"
|
|
142
|
+
slot "install_current()"
|
|
143
|
+
slot "refresh_updates()"
|
|
144
|
+
slot "set_update_source_selected()"
|
|
145
|
+
slot "update_selected()"
|
|
146
|
+
slot "update_all()"
|
|
147
|
+
slot "refresh_installed()"
|
|
148
|
+
slot "launch_selected()"
|
|
149
|
+
slot "info_selected()"
|
|
150
|
+
slot "change_title_selected()"
|
|
151
|
+
slot "change_icon_selected()"
|
|
152
|
+
slot "reset_selected()"
|
|
153
|
+
slot "sandbox_selected()"
|
|
154
|
+
slot "reinstall_selected()"
|
|
155
|
+
slot "uninstall_selected()"
|
|
156
|
+
slot "save_settings()"
|
|
157
|
+
slot "change_page(int)"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def initialize
|
|
161
|
+
super()
|
|
162
|
+
@store = ManifestStore.new
|
|
163
|
+
@customizer = AppCustomizer.new(store: @store)
|
|
164
|
+
@settings = Settings.new
|
|
165
|
+
@current_path = nil
|
|
166
|
+
@current_inspection = nil
|
|
167
|
+
|
|
168
|
+
set_window_title("Depot")
|
|
169
|
+
resize(980, 660)
|
|
170
|
+
set_window_icon(QIcon.new(Assets.logo_path)) if File.exist?(Assets.logo_path)
|
|
171
|
+
|
|
172
|
+
build_ui
|
|
173
|
+
load_settings_controls
|
|
174
|
+
apply_theme(@settings.load.fetch("theme", "system"))
|
|
175
|
+
refresh_installed
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def browse_file
|
|
179
|
+
file = QFileDialog.get_open_file_name(
|
|
180
|
+
self,
|
|
181
|
+
"Choose Software",
|
|
182
|
+
Dir.home,
|
|
183
|
+
"Software packages (*.AppImage *.appimage *.deb *.rpm *.flatpakref *.tar.gz *.tgz *.tar.xz *.txz *.tar.zst *.tzst *.zip);;All files (*)"
|
|
184
|
+
)
|
|
185
|
+
load_input(file) if file && !file.empty?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def inspect_current
|
|
189
|
+
return warn_dialog("Choose software first.") unless @current_path
|
|
190
|
+
|
|
191
|
+
result = Inspector.inspect(@current_path, checksum: false)
|
|
192
|
+
unless result.ok?
|
|
193
|
+
@current_inspection = nil
|
|
194
|
+
@detected_label.set_text("Could not inspect this file")
|
|
195
|
+
@detected_label.set_visible(true)
|
|
196
|
+
@summary.set_plain_text(result.error)
|
|
197
|
+
@install_button.set_enabled(false)
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
@current_inspection = result.value
|
|
202
|
+
@detected_label.set_text(detected_summary(@current_inspection))
|
|
203
|
+
@detected_label.set_visible(true)
|
|
204
|
+
@summary.set_plain_text(summary_for(@current_inspection))
|
|
205
|
+
@install_button.set_enabled(installable_inspection?(@current_inspection))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def install_current
|
|
209
|
+
return warn_dialog("Choose software first.") unless @current_path
|
|
210
|
+
|
|
211
|
+
inspect_current unless @current_inspection
|
|
212
|
+
return warn_dialog("No installer backend is available for this format yet.") unless installable_inspection?(@current_inspection)
|
|
213
|
+
|
|
214
|
+
answer = QMessageBox.question(
|
|
215
|
+
self,
|
|
216
|
+
"Install Software",
|
|
217
|
+
install_prompt(@current_inspection),
|
|
218
|
+
QMessageBox::Yes | QMessageBox::No
|
|
219
|
+
)
|
|
220
|
+
return unless answer == QMessageBox::Yes
|
|
221
|
+
|
|
222
|
+
result = with_install_progress("Installing #{install_progress_name(@current_inspection)}...\n\nThis may take a while.") do
|
|
223
|
+
Installer.install(@current_path, settings: @settings.load)
|
|
224
|
+
end
|
|
225
|
+
if result.ok?
|
|
226
|
+
manifest = result.value
|
|
227
|
+
QMessageBox.information(self, "Installed", "Installed #{manifest.fetch("display_name")} as #{manifest.fetch("app_id")}.")
|
|
228
|
+
@current_path = nil
|
|
229
|
+
@current_inspection = nil
|
|
230
|
+
@path_edit.set_text("")
|
|
231
|
+
@detected_label.set_text("")
|
|
232
|
+
@detected_label.set_visible(false)
|
|
233
|
+
@summary.set_plain_text("")
|
|
234
|
+
@install_button.set_enabled(false)
|
|
235
|
+
refresh_installed
|
|
236
|
+
@sidebar.set_current_row(1)
|
|
237
|
+
else
|
|
238
|
+
warn_dialog(result.error)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def refresh_installed
|
|
243
|
+
@apps_table.set_row_count(0)
|
|
244
|
+
@store.all.each_with_index do |manifest, row|
|
|
245
|
+
@apps_table.insert_row(row)
|
|
246
|
+
@apps_table.set_item(row, 0, QTableWidgetItem.new(manifest.fetch("app_id")))
|
|
247
|
+
@apps_table.set_item(row, 1, QTableWidgetItem.new(manifest.fetch("display_name")))
|
|
248
|
+
@apps_table.set_item(row, 2, QTableWidgetItem.new(manifest.fetch("backend")))
|
|
249
|
+
@apps_table.set_item(row, 3, QTableWidgetItem.new(manifest.fetch("installed_at", "")))
|
|
250
|
+
end
|
|
251
|
+
@apps_table.resize_columns_to_contents
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def launch_selected
|
|
255
|
+
manifest = selected_manifest
|
|
256
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
257
|
+
|
|
258
|
+
executable = Sandbox.launch_path(manifest, @settings.load)
|
|
259
|
+
Process.spawn(executable, pgroup: true)
|
|
260
|
+
rescue SystemCallError => e
|
|
261
|
+
warn_dialog("Could not launch app: #{e.message}")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def info_selected
|
|
265
|
+
manifest = selected_manifest
|
|
266
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
267
|
+
|
|
268
|
+
custom = manifest.fetch("customizations", {})
|
|
269
|
+
custom_icon = manifest["custom_icon"]
|
|
270
|
+
QMessageBox.information(
|
|
271
|
+
self,
|
|
272
|
+
manifest.fetch("display_name"),
|
|
273
|
+
[
|
|
274
|
+
"App ID: #{manifest.fetch("app_id")}",
|
|
275
|
+
"Name: #{manifest.fetch("display_name")}",
|
|
276
|
+
"Default name: #{manifest["default_display_name"] || manifest.fetch("display_name")}",
|
|
277
|
+
"Backend: #{manifest.fetch("backend")}",
|
|
278
|
+
"Executable: #{manifest.fetch("installed_executable")}",
|
|
279
|
+
"Desktop entry: #{manifest["desktop_entry"] || "none"}",
|
|
280
|
+
"Icon: #{active_icon_summary(manifest)}",
|
|
281
|
+
"Sandbox: #{Sandbox.summary(manifest, @settings.load)}",
|
|
282
|
+
"Custom title: #{custom["display_name"] ? "yes" : "no"}",
|
|
283
|
+
"Custom icon: #{custom_icon ? custom_icon["path"] : "no"}",
|
|
284
|
+
"Source: #{manifest.fetch("install_source")}",
|
|
285
|
+
"Update source: #{manifest.dig("update", "source") || "none"}",
|
|
286
|
+
"Installed: #{manifest.fetch("installed_at", "unknown")}"
|
|
287
|
+
].join("\n")
|
|
288
|
+
)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def change_title_selected
|
|
292
|
+
manifest = selected_manifest
|
|
293
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
294
|
+
|
|
295
|
+
ok = QBool.new
|
|
296
|
+
title = QInputDialog.get_text(
|
|
297
|
+
self,
|
|
298
|
+
QString.new("Change Title"),
|
|
299
|
+
QString.new("Application title:"),
|
|
300
|
+
QLineEdit::Normal,
|
|
301
|
+
QString.new(manifest.fetch("display_name")),
|
|
302
|
+
ok
|
|
303
|
+
)
|
|
304
|
+
return unless ok.ok? && title
|
|
305
|
+
|
|
306
|
+
result = @customizer.rename(manifest.fetch("app_id"), title.to_s)
|
|
307
|
+
if result.ok?
|
|
308
|
+
refresh_installed
|
|
309
|
+
status_bar.show_message(QString.new("Updated #{result.value.fetch("display_name")}"))
|
|
310
|
+
else
|
|
311
|
+
warn_dialog(result.error)
|
|
312
|
+
end
|
|
313
|
+
rescue StandardError => e
|
|
314
|
+
unexpected_error(e)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def change_icon_selected
|
|
318
|
+
manifest = selected_manifest
|
|
319
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
320
|
+
|
|
321
|
+
file = QFileDialog.get_open_file_name(
|
|
322
|
+
self,
|
|
323
|
+
"Choose Icon",
|
|
324
|
+
Dir.home,
|
|
325
|
+
"Icons (*.png *.svg *.xpm);;All files (*)"
|
|
326
|
+
)
|
|
327
|
+
return unless file && !file.empty?
|
|
328
|
+
|
|
329
|
+
result = @customizer.change_icon(manifest.fetch("app_id"), file)
|
|
330
|
+
if result.ok?
|
|
331
|
+
refresh_installed
|
|
332
|
+
status_bar.show_message(QString.new("Updated icon for #{result.value.fetch("display_name")}"))
|
|
333
|
+
else
|
|
334
|
+
warn_dialog(result.error)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def sandbox_selected
|
|
339
|
+
manifest = selected_manifest
|
|
340
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
341
|
+
|
|
342
|
+
if manifest["backend"] == "flatpak"
|
|
343
|
+
QMessageBox.information(self, "Sandbox", "Flatpak manages sandboxing for #{manifest.fetch("display_name")}.\n\nDepot will show Flatpak permissions in a later permission editor.")
|
|
344
|
+
return
|
|
345
|
+
end
|
|
346
|
+
return warn_dialog("Depot sandboxing is available for AppImage, portable Debian, portable RPM, and portable archive apps.") unless Sandbox.portable?(manifest)
|
|
347
|
+
|
|
348
|
+
dialog = SandboxDialog.new(self, manifest, @settings.load)
|
|
349
|
+
return unless dialog.exec == QDialog::Accepted
|
|
350
|
+
|
|
351
|
+
result = Sandbox.set(manifest.fetch("app_id"), dialog.values, store: @store, settings: @settings.load)
|
|
352
|
+
if result.ok?
|
|
353
|
+
refresh_installed
|
|
354
|
+
status_bar.show_message(QString.new("Updated sandbox for #{manifest.fetch("display_name")}"))
|
|
355
|
+
else
|
|
356
|
+
warn_dialog(result.error)
|
|
357
|
+
end
|
|
358
|
+
rescue StandardError => e
|
|
359
|
+
unexpected_error(e)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def reset_selected
|
|
363
|
+
manifest = selected_manifest
|
|
364
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
365
|
+
|
|
366
|
+
answer = QMessageBox.question(
|
|
367
|
+
self,
|
|
368
|
+
"Reset Properties",
|
|
369
|
+
"Reset #{manifest.fetch("display_name")} to its original title and icon?",
|
|
370
|
+
QMessageBox::Yes | QMessageBox::No
|
|
371
|
+
)
|
|
372
|
+
return unless answer == QMessageBox::Yes
|
|
373
|
+
|
|
374
|
+
result = @customizer.reset(manifest.fetch("app_id"))
|
|
375
|
+
if result.ok?
|
|
376
|
+
refresh_installed
|
|
377
|
+
status_bar.show_message(QString.new("Reset properties for #{result.value.fetch("display_name")}"))
|
|
378
|
+
else
|
|
379
|
+
warn_dialog(result.error)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def uninstall_selected
|
|
384
|
+
manifest = selected_manifest
|
|
385
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
386
|
+
|
|
387
|
+
answer = QMessageBox.question(
|
|
388
|
+
self,
|
|
389
|
+
"Uninstall",
|
|
390
|
+
"Remove #{manifest.fetch("display_name")} and its Depot-created desktop integration?",
|
|
391
|
+
QMessageBox::Yes | QMessageBox::No
|
|
392
|
+
)
|
|
393
|
+
return unless answer == QMessageBox::Yes
|
|
394
|
+
|
|
395
|
+
result = Uninstaller.uninstall(manifest.fetch("app_id"))
|
|
396
|
+
if result.ok?
|
|
397
|
+
QMessageBox.information(self, "Uninstalled", "Removed #{manifest.fetch("display_name")}.")
|
|
398
|
+
refresh_installed
|
|
399
|
+
else
|
|
400
|
+
warn_dialog(result.error)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def reinstall_selected
|
|
405
|
+
manifest = selected_manifest
|
|
406
|
+
return warn_dialog("Select an installed app first.") unless manifest
|
|
407
|
+
|
|
408
|
+
source = manifest["install_source"]
|
|
409
|
+
return warn_dialog("Depot does not know the original installer path for this app.") if source.to_s.empty?
|
|
410
|
+
source = resolved_install_source(source)
|
|
411
|
+
return warn_dialog("Original installer is missing: #{manifest["install_source"]}") unless source
|
|
412
|
+
|
|
413
|
+
answer = QMessageBox.question(
|
|
414
|
+
self,
|
|
415
|
+
"Reinstall",
|
|
416
|
+
"Reinstall #{manifest.fetch("display_name")} from its original installer?\n\nDepot will remove and recreate its managed files, desktop entry, icons, and manifest.",
|
|
417
|
+
QMessageBox::Yes | QMessageBox::No
|
|
418
|
+
)
|
|
419
|
+
return unless answer == QMessageBox::Yes
|
|
420
|
+
|
|
421
|
+
uninstall = nil
|
|
422
|
+
install = nil
|
|
423
|
+
with_install_progress("Reinstalling #{manifest.fetch("display_name")}...\n\nThis may take a while.") do
|
|
424
|
+
uninstall = Uninstaller.uninstall(manifest.fetch("app_id"))
|
|
425
|
+
install = Installer.install(source, settings: @settings.load) if uninstall.ok?
|
|
426
|
+
end
|
|
427
|
+
return warn_dialog(uninstall.error) unless uninstall.ok?
|
|
428
|
+
|
|
429
|
+
if install.ok?
|
|
430
|
+
refreshed = install.value
|
|
431
|
+
QMessageBox.information(self, "Reinstalled", "Reinstalled #{refreshed.fetch("display_name")} as #{refreshed.fetch("app_id")}.")
|
|
432
|
+
refresh_installed
|
|
433
|
+
else
|
|
434
|
+
warn_dialog("Depot removed the old install, but reinstall failed: #{install.error}")
|
|
435
|
+
refresh_installed
|
|
436
|
+
end
|
|
437
|
+
rescue StandardError => e
|
|
438
|
+
unexpected_error(e)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def refresh_updates
|
|
442
|
+
return unless @updates_table
|
|
443
|
+
|
|
444
|
+
enabled = @settings.load.fetch("updates_enabled", true)
|
|
445
|
+
@updates_status.set_text(enabled ? "Updates are enabled." : "Updates are disabled in Settings.")
|
|
446
|
+
@updates_table.set_row_count(0)
|
|
447
|
+
Updater.new(store: @store, settings: @settings).records.each_with_index do |record, row|
|
|
448
|
+
@updates_table.insert_row(row)
|
|
449
|
+
@updates_table.set_item(row, 0, QTableWidgetItem.new(record.fetch("app_id")))
|
|
450
|
+
@updates_table.set_item(row, 1, QTableWidgetItem.new(record.fetch("display_name")))
|
|
451
|
+
@updates_table.set_item(row, 2, QTableWidgetItem.new(record.fetch("method")))
|
|
452
|
+
@updates_table.set_item(row, 3, QTableWidgetItem.new(record.fetch("status")))
|
|
453
|
+
@updates_table.set_item(row, 4, QTableWidgetItem.new(record.fetch("last_updated_at").to_s))
|
|
454
|
+
end
|
|
455
|
+
@updates_table.resize_columns_to_contents
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def set_update_source_selected
|
|
459
|
+
app_id = selected_update_app_id
|
|
460
|
+
return warn_dialog("Select an app first.") unless app_id
|
|
461
|
+
|
|
462
|
+
manifest = @store.find(app_id)
|
|
463
|
+
current = manifest&.dig("update", "source").to_s
|
|
464
|
+
ok = QBool.new
|
|
465
|
+
url = QInputDialog.get_text(
|
|
466
|
+
self,
|
|
467
|
+
QString.new("Update Source"),
|
|
468
|
+
QString.new("HTTPS update URL:"),
|
|
469
|
+
QLineEdit::Normal,
|
|
470
|
+
QString.new(current.start_with?("https://") ? current : ""),
|
|
471
|
+
ok
|
|
472
|
+
)
|
|
473
|
+
return unless ok.ok? && url
|
|
474
|
+
|
|
475
|
+
result = Updater.new(store: @store, settings: @settings).set_source(app_id, url.to_s.strip)
|
|
476
|
+
if result.ok?
|
|
477
|
+
refresh_updates
|
|
478
|
+
status_bar.show_message(QString.new("Updated source for #{app_id}"))
|
|
479
|
+
else
|
|
480
|
+
warn_dialog(result.error)
|
|
481
|
+
end
|
|
482
|
+
rescue StandardError => e
|
|
483
|
+
unexpected_error(e)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def update_selected
|
|
487
|
+
return warn_dialog("Updates are disabled in Settings.") unless @settings.load.fetch("updates_enabled", true)
|
|
488
|
+
|
|
489
|
+
app_id = selected_update_app_id
|
|
490
|
+
return warn_dialog("Select an app to update first.") unless app_id
|
|
491
|
+
|
|
492
|
+
result = with_install_progress("Updating #{app_id}...\n\nThis may take a while.") do
|
|
493
|
+
Updater.new(store: @store, settings: @settings).update(app_id)
|
|
494
|
+
end
|
|
495
|
+
if result.ok?
|
|
496
|
+
QMessageBox.information(self, "Updated", "Updated #{app_id}.")
|
|
497
|
+
refresh_updates
|
|
498
|
+
refresh_installed
|
|
499
|
+
else
|
|
500
|
+
warn_dialog(result.error)
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def update_all
|
|
505
|
+
return warn_dialog("Updates are disabled in Settings.") unless @settings.load.fetch("updates_enabled", true)
|
|
506
|
+
|
|
507
|
+
answer = QMessageBox.question(
|
|
508
|
+
self,
|
|
509
|
+
"Update All",
|
|
510
|
+
"Update every app that Depot knows how to update?",
|
|
511
|
+
QMessageBox::Yes | QMessageBox::No
|
|
512
|
+
)
|
|
513
|
+
return unless answer == QMessageBox::Yes
|
|
514
|
+
|
|
515
|
+
result = with_install_progress("Updating apps...\n\nThis may take a while.") do
|
|
516
|
+
Updater.new(store: @store, settings: @settings).update_all
|
|
517
|
+
end
|
|
518
|
+
if result.ok?
|
|
519
|
+
QMessageBox.information(self, "Updates", "Finished updating apps.")
|
|
520
|
+
else
|
|
521
|
+
warn_dialog(([result.error] + result.warnings).join("\n"))
|
|
522
|
+
end
|
|
523
|
+
refresh_updates
|
|
524
|
+
refresh_installed
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def save_settings
|
|
528
|
+
values = {
|
|
529
|
+
"warning_verbosity" => @warning_combo.current_text,
|
|
530
|
+
"theme" => @theme_combo.current_text,
|
|
531
|
+
"default_install_location" => "user",
|
|
532
|
+
"sandbox_preference" => @sandbox_combo.current_text,
|
|
533
|
+
"sandbox_profile" => @sandbox_profile_combo.current_text,
|
|
534
|
+
"sandbox_home_access" => @sandbox_home_combo.current_text,
|
|
535
|
+
"sandbox_network" => @sandbox_network_check.checked?,
|
|
536
|
+
"desktop_integration" => @desktop_check.checked?,
|
|
537
|
+
"updates_enabled" => @updates_check.checked?
|
|
538
|
+
}
|
|
539
|
+
@settings.save(values)
|
|
540
|
+
apply_theme(values.fetch("theme"))
|
|
541
|
+
QMessageBox.information(self, "Settings", "Depot settings were saved.")
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def change_page(index)
|
|
545
|
+
@pages.set_current_index(index)
|
|
546
|
+
refresh_installed if index == 1
|
|
547
|
+
refresh_updates if index == 2
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def load_input(path)
|
|
551
|
+
@current_path = path
|
|
552
|
+
@path_edit.set_text(path)
|
|
553
|
+
inspect_current
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
private
|
|
557
|
+
|
|
558
|
+
def build_ui
|
|
559
|
+
central = QWidget.new
|
|
560
|
+
root = QHBoxLayout.new
|
|
561
|
+
|
|
562
|
+
@sidebar = QListWidget.new
|
|
563
|
+
@sidebar.set_fixed_width(170)
|
|
564
|
+
@sidebar.set_object_name("depotSidebar")
|
|
565
|
+
%w[Install Installed Updates Settings About].each { |name| @sidebar.add_item(name) }
|
|
566
|
+
@sidebar.current_row_changed.connect(self, :change_page)
|
|
567
|
+
|
|
568
|
+
@pages = QStackedWidget.new
|
|
569
|
+
@pages.add_widget(build_install_page)
|
|
570
|
+
@pages.add_widget(build_installed_page)
|
|
571
|
+
@pages.add_widget(build_updates_page)
|
|
572
|
+
@pages.add_widget(build_settings_page)
|
|
573
|
+
@pages.add_widget(build_about_page)
|
|
574
|
+
|
|
575
|
+
root.add_widget(@sidebar)
|
|
576
|
+
root.add_widget(@pages, 1)
|
|
577
|
+
central.set_layout(root)
|
|
578
|
+
set_central_widget(central)
|
|
579
|
+
status_bar.show_message(QString.new("Ready"))
|
|
580
|
+
@sidebar.set_current_row(0)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def build_install_page
|
|
584
|
+
page = QWidget.new
|
|
585
|
+
layout = QVBoxLayout.new
|
|
586
|
+
|
|
587
|
+
logo_path = Assets.logo_path
|
|
588
|
+
if File.exist?(logo_path)
|
|
589
|
+
logo = QLabel.new
|
|
590
|
+
pixmap = QPixmap.new(logo_path)
|
|
591
|
+
logo.set_pixmap(pixmap.scaled(96, 96, Qt::KeepAspectRatio, Qt::SmoothTransformation))
|
|
592
|
+
logo.set_alignment(Qt::AlignCenter)
|
|
593
|
+
layout.add_widget(logo)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
title = QLabel.new("Install software")
|
|
597
|
+
title.set_style_sheet("font-size: 26px; font-weight: 800;")
|
|
598
|
+
title.set_alignment(Qt::AlignCenter)
|
|
599
|
+
layout.add_widget(title)
|
|
600
|
+
|
|
601
|
+
subtitle = QLabel.new("Select any Linux programs you'd like to install.")
|
|
602
|
+
subtitle.set_object_name("depotInstallSubtitle")
|
|
603
|
+
subtitle.set_alignment(Qt::AlignCenter)
|
|
604
|
+
layout.add_widget(subtitle)
|
|
605
|
+
|
|
606
|
+
drop = DropPanel.new { |path| load_input(path) }
|
|
607
|
+
layout.add_widget(drop)
|
|
608
|
+
|
|
609
|
+
row = QHBoxLayout.new
|
|
610
|
+
@path_edit = QLineEdit.new
|
|
611
|
+
@path_edit.set_placeholder_text("Path to package or installer")
|
|
612
|
+
browse = QPushButton.new("Choose File")
|
|
613
|
+
browse.clicked.connect(self, :browse_file)
|
|
614
|
+
inspect = QPushButton.new("Inspect")
|
|
615
|
+
inspect.clicked.connect(self, :inspect_current)
|
|
616
|
+
row.add_widget(@path_edit, 1)
|
|
617
|
+
row.add_widget(browse)
|
|
618
|
+
row.add_widget(inspect)
|
|
619
|
+
layout.add_layout(row)
|
|
620
|
+
|
|
621
|
+
@detected_label = QLabel.new("")
|
|
622
|
+
@detected_label.set_object_name("depotDetectedLabel")
|
|
623
|
+
@detected_label.set_word_wrap(true)
|
|
624
|
+
@detected_label.set_visible(false)
|
|
625
|
+
layout.add_widget(@detected_label)
|
|
626
|
+
|
|
627
|
+
@summary = QTextEdit.new
|
|
628
|
+
@summary.set_read_only(true)
|
|
629
|
+
@summary.set_minimum_height(220)
|
|
630
|
+
layout.add_widget(@summary, 1)
|
|
631
|
+
|
|
632
|
+
@install_button = QPushButton.new("Install Software")
|
|
633
|
+
@install_button.set_enabled(false)
|
|
634
|
+
@install_button.clicked.connect(self, :install_current)
|
|
635
|
+
layout.add_widget(@install_button)
|
|
636
|
+
|
|
637
|
+
page.set_layout(layout)
|
|
638
|
+
page
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def build_installed_page
|
|
642
|
+
page = QWidget.new
|
|
643
|
+
layout = QVBoxLayout.new
|
|
644
|
+
|
|
645
|
+
title = QLabel.new("Installed Apps")
|
|
646
|
+
title.set_style_sheet("font-size: 24px; font-weight: 700;")
|
|
647
|
+
layout.add_widget(title)
|
|
648
|
+
|
|
649
|
+
@apps_table = InstalledAppsTable.new(self)
|
|
650
|
+
@apps_table.set_horizontal_header_labels(QStringList.new << "ID" << "Name" << "Backend" << "Installed")
|
|
651
|
+
@apps_table.set_selection_behavior(QAbstractItemView::SelectRows)
|
|
652
|
+
@apps_table.set_selection_mode(QAbstractItemView::SingleSelection)
|
|
653
|
+
layout.add_widget(@apps_table, 1)
|
|
654
|
+
|
|
655
|
+
buttons = QHBoxLayout.new
|
|
656
|
+
refresh = QPushButton.new("Refresh")
|
|
657
|
+
refresh.clicked.connect(self, :refresh_installed)
|
|
658
|
+
launch = QPushButton.new("Launch")
|
|
659
|
+
launch.clicked.connect(self, :launch_selected)
|
|
660
|
+
info = QPushButton.new("Info")
|
|
661
|
+
info.clicked.connect(self, :info_selected)
|
|
662
|
+
rename = QPushButton.new("Rename")
|
|
663
|
+
rename.clicked.connect(self, :change_title_selected)
|
|
664
|
+
icon = QPushButton.new("Icon")
|
|
665
|
+
icon.clicked.connect(self, :change_icon_selected)
|
|
666
|
+
sandbox = QPushButton.new("Sandbox")
|
|
667
|
+
sandbox.clicked.connect(self, :sandbox_selected)
|
|
668
|
+
reset = QPushButton.new("Reset Properties")
|
|
669
|
+
reset.clicked.connect(self, :reset_selected)
|
|
670
|
+
reinstall = QPushButton.new("Reinstall")
|
|
671
|
+
reinstall.clicked.connect(self, :reinstall_selected)
|
|
672
|
+
uninstall = QPushButton.new("Uninstall")
|
|
673
|
+
uninstall.clicked.connect(self, :uninstall_selected)
|
|
674
|
+
buttons.add_widget(refresh)
|
|
675
|
+
buttons.add_stretch
|
|
676
|
+
buttons.add_widget(launch)
|
|
677
|
+
buttons.add_widget(info)
|
|
678
|
+
buttons.add_widget(rename)
|
|
679
|
+
buttons.add_widget(icon)
|
|
680
|
+
buttons.add_widget(sandbox)
|
|
681
|
+
buttons.add_widget(reset)
|
|
682
|
+
buttons.add_widget(reinstall)
|
|
683
|
+
buttons.add_widget(uninstall)
|
|
684
|
+
layout.add_layout(buttons)
|
|
685
|
+
|
|
686
|
+
page.set_layout(layout)
|
|
687
|
+
page
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def build_updates_page
|
|
691
|
+
page = QWidget.new
|
|
692
|
+
layout = QVBoxLayout.new
|
|
693
|
+
|
|
694
|
+
title = QLabel.new("Updates")
|
|
695
|
+
title.set_style_sheet("font-size: 24px; font-weight: 700;")
|
|
696
|
+
layout.add_widget(title)
|
|
697
|
+
|
|
698
|
+
@updates_status = QLabel.new("Updates are enabled.")
|
|
699
|
+
@updates_status.set_object_name("depotInstallSubtitle")
|
|
700
|
+
layout.add_widget(@updates_status)
|
|
701
|
+
|
|
702
|
+
@updates_table = QTableWidget.new(0, 5)
|
|
703
|
+
@updates_table.set_horizontal_header_labels(QStringList.new << "ID" << "Name" << "Method" << "Status" << "Last Updated")
|
|
704
|
+
@updates_table.set_selection_behavior(QAbstractItemView::SelectRows)
|
|
705
|
+
@updates_table.set_selection_mode(QAbstractItemView::SingleSelection)
|
|
706
|
+
layout.add_widget(@updates_table, 1)
|
|
707
|
+
|
|
708
|
+
buttons = QHBoxLayout.new
|
|
709
|
+
refresh = QPushButton.new("Refresh")
|
|
710
|
+
refresh.clicked.connect(self, :refresh_updates)
|
|
711
|
+
set_url = QPushButton.new("Set URL")
|
|
712
|
+
set_url.clicked.connect(self, :set_update_source_selected)
|
|
713
|
+
selected = QPushButton.new("Update Selected")
|
|
714
|
+
selected.clicked.connect(self, :update_selected)
|
|
715
|
+
all = QPushButton.new("Update All")
|
|
716
|
+
all.clicked.connect(self, :update_all)
|
|
717
|
+
buttons.add_widget(refresh)
|
|
718
|
+
buttons.add_stretch
|
|
719
|
+
buttons.add_widget(set_url)
|
|
720
|
+
buttons.add_widget(selected)
|
|
721
|
+
buttons.add_widget(all)
|
|
722
|
+
layout.add_layout(buttons)
|
|
723
|
+
|
|
724
|
+
page.set_layout(layout)
|
|
725
|
+
page
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def build_settings_page
|
|
729
|
+
page = QWidget.new
|
|
730
|
+
layout = QVBoxLayout.new
|
|
731
|
+
|
|
732
|
+
title = QLabel.new("Settings")
|
|
733
|
+
title.set_style_sheet("font-size: 24px; font-weight: 700;")
|
|
734
|
+
layout.add_widget(title)
|
|
735
|
+
|
|
736
|
+
form = QFormLayout.new
|
|
737
|
+
@warning_combo = QComboBox.new
|
|
738
|
+
%w[quiet normal detailed].each { |value| @warning_combo.add_item(value) }
|
|
739
|
+
@theme_combo = QComboBox.new
|
|
740
|
+
%w[system light dark].each { |value| @theme_combo.add_item(value) }
|
|
741
|
+
@sandbox_combo = QComboBox.new
|
|
742
|
+
%w[ask prefer-off prefer-on].each { |value| @sandbox_combo.add_item(value) }
|
|
743
|
+
@sandbox_profile_combo = QComboBox.new
|
|
744
|
+
%w[relaxed balanced strict].each { |value| @sandbox_profile_combo.add_item(value) }
|
|
745
|
+
@sandbox_home_combo = QComboBox.new
|
|
746
|
+
%w[isolated documents full].each { |value| @sandbox_home_combo.add_item(value) }
|
|
747
|
+
@sandbox_network_check = QCheckBox.new("Allow network access in new app sandboxes")
|
|
748
|
+
@desktop_check = QCheckBox.new("Create desktop launchers and icons")
|
|
749
|
+
@updates_check = QCheckBox.new("Enable updates")
|
|
750
|
+
|
|
751
|
+
form.add_row(QString.new("Warnings"), @warning_combo)
|
|
752
|
+
form.add_row(QString.new("Theme"), @theme_combo)
|
|
753
|
+
form.add_row(QString.new("Sandbox default"), @sandbox_combo)
|
|
754
|
+
form.add_row(QString.new("Sandbox profile"), @sandbox_profile_combo)
|
|
755
|
+
form.add_row(QString.new("Sandbox home"), @sandbox_home_combo)
|
|
756
|
+
form.add_row(QString.new("Sandbox network"), @sandbox_network_check)
|
|
757
|
+
form.add_row(QString.new("Desktop integration"), @desktop_check)
|
|
758
|
+
form.add_row(QString.new("Updates"), @updates_check)
|
|
759
|
+
layout.add_layout(form)
|
|
760
|
+
|
|
761
|
+
save = QPushButton.new("Save Settings")
|
|
762
|
+
save.clicked.connect(self, :save_settings)
|
|
763
|
+
layout.add_widget(save)
|
|
764
|
+
layout.add_stretch
|
|
765
|
+
|
|
766
|
+
page.set_layout(layout)
|
|
767
|
+
page
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def build_about_page
|
|
771
|
+
page = QWidget.new
|
|
772
|
+
layout = QVBoxLayout.new
|
|
773
|
+
logo_path = Assets.logo_path
|
|
774
|
+
if File.exist?(logo_path)
|
|
775
|
+
logo = QLabel.new
|
|
776
|
+
pixmap = QPixmap.new(logo_path)
|
|
777
|
+
logo.set_pixmap(pixmap.scaled(164, 164, Qt::KeepAspectRatio, Qt::SmoothTransformation))
|
|
778
|
+
logo.set_alignment(Qt::AlignCenter)
|
|
779
|
+
layout.add_widget(logo)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
text = QLabel.new("Depot\nUniversal Linux application installer and desktop integration layer.\n\nThis build installs software through Depot manifests, desktop launchers, icons, settings, and clean uninstall tracking. Additional backends plug into the same flow.")
|
|
783
|
+
text.set_alignment(Qt::AlignCenter)
|
|
784
|
+
text.set_word_wrap(true)
|
|
785
|
+
layout.add_widget(text)
|
|
786
|
+
layout.add_stretch
|
|
787
|
+
page.set_layout(layout)
|
|
788
|
+
page
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def load_settings_controls
|
|
792
|
+
values = @settings.load
|
|
793
|
+
set_combo(@warning_combo, values.fetch("warning_verbosity", "normal"))
|
|
794
|
+
set_combo(@theme_combo, values.fetch("theme", "system"))
|
|
795
|
+
set_combo(@sandbox_combo, values.fetch("sandbox_preference", "ask"))
|
|
796
|
+
set_combo(@sandbox_profile_combo, values.fetch("sandbox_profile", "balanced"))
|
|
797
|
+
set_combo(@sandbox_home_combo, values.fetch("sandbox_home_access", "documents"))
|
|
798
|
+
@sandbox_network_check.set_checked(values.fetch("sandbox_network", true))
|
|
799
|
+
@desktop_check.set_checked(values.fetch("desktop_integration", true))
|
|
800
|
+
@updates_check.set_checked(values.fetch("updates_enabled", true))
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def set_combo(combo, value)
|
|
804
|
+
index = combo.find_text(value)
|
|
805
|
+
combo.set_current_index(index) if index && index >= 0
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def apply_theme(theme)
|
|
809
|
+
set_style_sheet(base_stylesheet(theme))
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def base_stylesheet(theme)
|
|
813
|
+
palette = case theme
|
|
814
|
+
when "dark"
|
|
815
|
+
{
|
|
816
|
+
bg: "#17191d", panel: "#20242a", field: "#242932", text: "#f2f4f8",
|
|
817
|
+
muted: "#aeb6c2", border: "#3b424d", accent: "#2f6fed", accent_hover: "#3f7cff"
|
|
818
|
+
}
|
|
819
|
+
when "light"
|
|
820
|
+
{
|
|
821
|
+
bg: "#f5f6f8", panel: "#ffffff", field: "#ffffff", text: "#17191d",
|
|
822
|
+
muted: "#5f6875", border: "#cfd5dd", accent: "#245fd6", accent_hover: "#1f55bf"
|
|
823
|
+
}
|
|
824
|
+
else
|
|
825
|
+
{
|
|
826
|
+
bg: "#f5f6f8", panel: "#ffffff", field: "#ffffff", text: "#17191d",
|
|
827
|
+
muted: "#5f6875", border: "#cfd5dd", accent: "#245fd6", accent_hover: "#1f55bf"
|
|
828
|
+
}
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
<<~CSS
|
|
832
|
+
QMainWindow, QWidget {
|
|
833
|
+
background: #{palette.fetch(:bg)};
|
|
834
|
+
color: #{palette.fetch(:text)};
|
|
835
|
+
font-size: 14px;
|
|
836
|
+
}
|
|
837
|
+
QLabel#depotDropTitle {
|
|
838
|
+
font-size: 22px;
|
|
839
|
+
font-weight: 700;
|
|
840
|
+
}
|
|
841
|
+
QLabel#depotDropHint {
|
|
842
|
+
color: #{palette.fetch(:muted)};
|
|
843
|
+
}
|
|
844
|
+
QLabel#depotInstallSubtitle {
|
|
845
|
+
color: #{palette.fetch(:muted)};
|
|
846
|
+
font-size: 14px;
|
|
847
|
+
}
|
|
848
|
+
QLabel#depotDetectedLabel {
|
|
849
|
+
background: #{palette.fetch(:panel)};
|
|
850
|
+
color: #{palette.fetch(:text)};
|
|
851
|
+
border: 1px solid #{palette.fetch(:border)};
|
|
852
|
+
border-radius: 6px;
|
|
853
|
+
padding: 10px 12px;
|
|
854
|
+
font-weight: 700;
|
|
855
|
+
}
|
|
856
|
+
QFrame#depotDropPanel {
|
|
857
|
+
background: #{palette.fetch(:panel)};
|
|
858
|
+
border: 2px dashed #{palette.fetch(:border)};
|
|
859
|
+
border-radius: 8px;
|
|
860
|
+
}
|
|
861
|
+
QFrame#depotDropPanel[dragActive="true"] {
|
|
862
|
+
border-color: #{palette.fetch(:accent)};
|
|
863
|
+
background: #{palette.fetch(:field)};
|
|
864
|
+
}
|
|
865
|
+
QListWidget#depotSidebar, QTextEdit, QLineEdit, QTableWidget, QComboBox {
|
|
866
|
+
background: #{palette.fetch(:field)};
|
|
867
|
+
color: #{palette.fetch(:text)};
|
|
868
|
+
border: 1px solid #{palette.fetch(:border)};
|
|
869
|
+
border-radius: 6px;
|
|
870
|
+
}
|
|
871
|
+
QListWidget#depotSidebar::item {
|
|
872
|
+
min-height: 38px;
|
|
873
|
+
padding-left: 12px;
|
|
874
|
+
border-radius: 4px;
|
|
875
|
+
}
|
|
876
|
+
QListWidget#depotSidebar::item:selected {
|
|
877
|
+
background: #{palette.fetch(:accent)};
|
|
878
|
+
color: white;
|
|
879
|
+
}
|
|
880
|
+
QPushButton {
|
|
881
|
+
background: #{palette.fetch(:accent)};
|
|
882
|
+
color: white;
|
|
883
|
+
padding: 8px 14px;
|
|
884
|
+
border: 0;
|
|
885
|
+
border-radius: 6px;
|
|
886
|
+
}
|
|
887
|
+
QPushButton:hover {
|
|
888
|
+
background: #{palette.fetch(:accent_hover)};
|
|
889
|
+
}
|
|
890
|
+
QPushButton:disabled {
|
|
891
|
+
background: #{palette.fetch(:border)};
|
|
892
|
+
color: #{palette.fetch(:muted)};
|
|
893
|
+
}
|
|
894
|
+
CSS
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def summary_for(inspection)
|
|
898
|
+
lines = [
|
|
899
|
+
"Package",
|
|
900
|
+
" Name: #{inspection.display_name}",
|
|
901
|
+
" Format: #{inspection.format} (#{inspection.confidence})",
|
|
902
|
+
" Size: #{format_bytes(inspection.size)}",
|
|
903
|
+
" SHA-256: #{inspection.sha256 || "calculated during install"}",
|
|
904
|
+
" Executable file: #{inspection.executable ? "yes" : "no"}"
|
|
905
|
+
]
|
|
906
|
+
if inspection.deb?
|
|
907
|
+
dependencies = dependency_names(inspection.metadata["depends"])
|
|
908
|
+
scripts = inspection.metadata.fetch("maintainer_scripts", [])
|
|
909
|
+
desktops = inspection.metadata.fetch("desktop_entries", [])
|
|
910
|
+
lines << ""
|
|
911
|
+
lines << "Debian package:"
|
|
912
|
+
lines << " Package: #{inspection.metadata["package"] || "unknown"}"
|
|
913
|
+
lines << " Version: #{inspection.metadata["version"] || "unknown"}"
|
|
914
|
+
lines << " Architecture: #{inspection.metadata["architecture"] || "unknown"}"
|
|
915
|
+
lines << " Dependencies: #{dependencies.empty? ? "none declared" : dependency_summary(dependencies)}"
|
|
916
|
+
lines << " Maintainer scripts: #{scripts.empty? ? "none" : scripts.join(", ")}"
|
|
917
|
+
lines << " Desktop entries: #{desktops.empty? ? "none" : "#{desktops.length} found; using #{inspection.metadata["primary_desktop_entry"]}"}"
|
|
918
|
+
lines << " Portable install: extracts into Depot; does not run apt, dpkg, sudo, or maintainer scripts"
|
|
919
|
+
elsif inspection.archive?
|
|
920
|
+
scripts = inspection.metadata.fetch("script_entries", [])
|
|
921
|
+
markers = inspection.metadata.fetch("source_markers", [])
|
|
922
|
+
executables = inspection.metadata.fetch("executable_candidates", [])
|
|
923
|
+
desktops = inspection.metadata.fetch("desktop_entries", [])
|
|
924
|
+
lines << ""
|
|
925
|
+
lines << "Portable archive:"
|
|
926
|
+
lines << " Archive root: #{inspection.metadata["archive_root"] || "mixed"}"
|
|
927
|
+
lines << " Executable candidates: #{executables.empty? ? "none" : executables.first(6).join(", ")}"
|
|
928
|
+
lines << " Desktop entries: #{desktops.empty? ? "none; Depot will generate one if possible" : "#{desktops.length} found; using #{inspection.metadata["primary_desktop_entry"]}"}"
|
|
929
|
+
lines << " Icons/images: #{inspection.metadata.fetch("icon_count", 0)} found"
|
|
930
|
+
lines << " Installer-like scripts: #{scripts.empty? ? "none" : scripts.first(6).join(", ")}"
|
|
931
|
+
lines << " Source/build markers: #{markers.empty? ? "none" : markers.join(", ")}"
|
|
932
|
+
lines << " Portable install: extracts into Depot; does not run scripts"
|
|
933
|
+
elsif inspection.rpm?
|
|
934
|
+
requirements = rpm_requirement_names(inspection.metadata["requires"])
|
|
935
|
+
scriptlets = inspection.metadata.fetch("scriptlets", [])
|
|
936
|
+
desktops = inspection.metadata.fetch("desktop_entries", [])
|
|
937
|
+
lines << ""
|
|
938
|
+
lines << "RPM package:"
|
|
939
|
+
lines << " Package: #{inspection.metadata["package"] || "unknown"}"
|
|
940
|
+
lines << " Version: #{rpm_version_label(inspection.metadata)}"
|
|
941
|
+
lines << " Architecture: #{inspection.metadata["architecture"] || "unknown"}"
|
|
942
|
+
lines << " Payload: #{inspection.metadata["payload_format"] || "unknown"} / #{inspection.metadata["payload_compressor"] || "unknown"}"
|
|
943
|
+
lines << " Requirements: #{requirements.empty? ? "none declared" : dependency_summary(requirements)}"
|
|
944
|
+
lines << " Scriptlets: #{scriptlets.empty? ? "none" : scriptlets.join(", ")}"
|
|
945
|
+
lines << " Desktop entries: #{desktops.empty? ? "none" : "#{desktops.length} found; using #{inspection.metadata["primary_desktop_entry"]}"}"
|
|
946
|
+
lines << " Portable install: extracts into Depot; does not run rpm, dnf, zypper, sudo, or scriptlets"
|
|
947
|
+
elsif inspection.flatpakref?
|
|
948
|
+
lines << ""
|
|
949
|
+
lines << "Flatpak reference:"
|
|
950
|
+
lines << " Flatpak ID: #{inspection.metadata["name"] || "unknown"}"
|
|
951
|
+
lines << " Branch: #{inspection.metadata["branch"] || "master"}"
|
|
952
|
+
lines << " Remote: #{inspection.metadata["suggest_remote_name"] || "none"}"
|
|
953
|
+
lines << " URL: #{inspection.metadata["url"] || "unknown"}"
|
|
954
|
+
lines << " Runtime ref: #{inspection.metadata["is_runtime"] ? "yes" : "no"}"
|
|
955
|
+
lines << " Embedded GPG key: #{inspection.metadata["gpg_key_present"] ? "yes" : "no"}"
|
|
956
|
+
lines << " Install mode: Flatpak handles download, runtime dependencies, sandboxing, and desktop integration"
|
|
957
|
+
end
|
|
958
|
+
lines << ""
|
|
959
|
+
lines << "Warnings:"
|
|
960
|
+
lines.concat(list_or_none(inspection.warnings))
|
|
961
|
+
lines << ""
|
|
962
|
+
lines << "Risks:"
|
|
963
|
+
lines.concat(list_or_none(inspection.risks))
|
|
964
|
+
lines.join("\n")
|
|
965
|
+
end
|
|
966
|
+
|
|
967
|
+
def list_or_none(items)
|
|
968
|
+
return [" none"] if items.empty?
|
|
969
|
+
|
|
970
|
+
items.map { |item| " - #{item}" }
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
def selected_manifest
|
|
974
|
+
row = @apps_table.current_row
|
|
975
|
+
return nil if row.nil? || row.negative?
|
|
976
|
+
|
|
977
|
+
item = @apps_table.item(row, 0)
|
|
978
|
+
return nil unless item
|
|
979
|
+
|
|
980
|
+
@store.find(item.text)
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def selected_update_app_id
|
|
984
|
+
row = @updates_table.current_row
|
|
985
|
+
return nil if row.nil? || row.negative?
|
|
986
|
+
|
|
987
|
+
item = @updates_table.item(row, 0)
|
|
988
|
+
item&.text
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
def resolved_install_source(source)
|
|
992
|
+
SourceResolver.resolve(source)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
def installable_inspection?(inspection)
|
|
996
|
+
inspection&.appimage? || inspection&.deb? || inspection&.archive? || inspection&.rpm? || inspection&.flatpakref?
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def install_prompt(inspection)
|
|
1000
|
+
base_actions = [
|
|
1001
|
+
"Record a Depot manifest",
|
|
1002
|
+
"Prepare desktop integration",
|
|
1003
|
+
"Track uninstall behavior"
|
|
1004
|
+
]
|
|
1005
|
+
return prompt_message(inspection, base_actions.unshift("Copy the app into your user application folder"), []) unless inspection.deb? || inspection.archive? || inspection.rpm? || inspection.flatpakref?
|
|
1006
|
+
|
|
1007
|
+
if inspection.archive?
|
|
1008
|
+
scripts = inspection.metadata.fetch("script_entries", [])
|
|
1009
|
+
markers = inspection.metadata.fetch("source_markers", [])
|
|
1010
|
+
actions = ["Extract the archive user-locally", "Infer launcher/icon integration", "Record uninstall tracking"]
|
|
1011
|
+
notes = ["Installer scripts are not run"]
|
|
1012
|
+
notes << "Scripts found: #{scripts.first(6).join(", ")}" unless scripts.empty?
|
|
1013
|
+
notes << "Source/build markers found: #{markers.join(", ")}" unless markers.empty?
|
|
1014
|
+
return prompt_message(inspection, actions, notes)
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
if inspection.rpm?
|
|
1018
|
+
requirements = rpm_requirement_names(inspection.metadata["requires"])
|
|
1019
|
+
scriptlets = inspection.metadata.fetch("scriptlets", [])
|
|
1020
|
+
actions = ["Extract the RPM payload user-locally", "Rewrite launchers/icons for Depot", "Record uninstall tracking"]
|
|
1021
|
+
notes = ["No rpm, dnf, zypper, sudo, or scriptlets"]
|
|
1022
|
+
notes << "Requirements are not installed automatically: #{dependency_summary(requirements)}" unless requirements.empty?
|
|
1023
|
+
notes << "Scriptlets will not be run: #{scriptlets.join(", ")}" unless scriptlets.empty?
|
|
1024
|
+
return prompt_message(inspection, actions, notes)
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
if inspection.flatpakref?
|
|
1028
|
+
actions = ["Install through Flatpak in user mode", "Track the app in Depot", "Use Flatpak for launch/uninstall"]
|
|
1029
|
+
notes = [
|
|
1030
|
+
"Flatpak ID: #{inspection.metadata["name"] || inspection.display_name}",
|
|
1031
|
+
"Remote: #{inspection.metadata["suggest_remote_name"] || "none"}",
|
|
1032
|
+
"Flatpak manages sandboxing and runtime dependencies"
|
|
1033
|
+
]
|
|
1034
|
+
return prompt_message(inspection, actions, notes)
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
dependencies = dependency_names(inspection.metadata["depends"])
|
|
1038
|
+
scripts = inspection.metadata.fetch("maintainer_scripts", [])
|
|
1039
|
+
actions = ["Extract the Debian payload user-locally", "Rewrite launchers/icons for Depot", "Record uninstall tracking"]
|
|
1040
|
+
notes = ["No apt, dpkg, sudo, or maintainer scripts"]
|
|
1041
|
+
notes << "Dependencies are not installed automatically: #{dependency_summary(dependencies)}" unless dependencies.empty?
|
|
1042
|
+
notes << "Scripts will not be run: #{scripts.join(", ")}" unless scripts.empty?
|
|
1043
|
+
prompt_message(inspection, actions, notes)
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
def prompt_message(inspection, actions, notes)
|
|
1047
|
+
[
|
|
1048
|
+
"#{inspection.display_name}",
|
|
1049
|
+
"#{inspection.format} install",
|
|
1050
|
+
"",
|
|
1051
|
+
"What Depot will do:",
|
|
1052
|
+
*actions.map { |action| " - #{action}" },
|
|
1053
|
+
("Notes:" unless notes.empty?),
|
|
1054
|
+
*notes.map { |note| " - #{note}" },
|
|
1055
|
+
"",
|
|
1056
|
+
"Install #{inspection.metadata["package"] || inspection.metadata["name"] || inspection.display_name}?"
|
|
1057
|
+
].compact.join("\n")
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def detected_summary(inspection)
|
|
1061
|
+
backend = case inspection.format
|
|
1062
|
+
when "flatpakref" then "Flatpak"
|
|
1063
|
+
when "deb" then "Debian portable"
|
|
1064
|
+
when "rpm" then "RPM portable"
|
|
1065
|
+
when "appimage" then "AppImage"
|
|
1066
|
+
when "tar.gz", "tar.xz", "tar.zst" then "Portable archive"
|
|
1067
|
+
else inspection.format
|
|
1068
|
+
end
|
|
1069
|
+
"#{backend} detected: #{inspection.display_name} - #{format_bytes(inspection.size)}"
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
def format_bytes(size)
|
|
1073
|
+
return "unknown" unless size
|
|
1074
|
+
|
|
1075
|
+
units = %w[B KB MB GB]
|
|
1076
|
+
value = size.to_f
|
|
1077
|
+
unit = units.shift
|
|
1078
|
+
while value >= 1024 && units.any?
|
|
1079
|
+
value /= 1024
|
|
1080
|
+
unit = units.shift
|
|
1081
|
+
end
|
|
1082
|
+
value >= 10 || unit == "B" ? "#{value.round} #{unit}" : "#{value.round(1)} #{unit}"
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
def with_install_progress(message)
|
|
1086
|
+
dialog = QMessageBox.new(
|
|
1087
|
+
QMessageBox::Information,
|
|
1088
|
+
QString.new("Depot"),
|
|
1089
|
+
QString.new(message),
|
|
1090
|
+
QMessageBox::NoButton.to_qflags,
|
|
1091
|
+
self
|
|
1092
|
+
)
|
|
1093
|
+
dialog.set_option(QMessageBox::DontUseNativeDialog, true)
|
|
1094
|
+
dialog.set_window_title(QString.new("Depot"))
|
|
1095
|
+
dialog.set_modal(true)
|
|
1096
|
+
dialog.set_window_modality(Qt::ApplicationModal)
|
|
1097
|
+
dialog.set_style_sheet(install_progress_message_style)
|
|
1098
|
+
dialog.set_fixed_size(460, 150)
|
|
1099
|
+
dialog.show
|
|
1100
|
+
QApplication.process_events
|
|
1101
|
+
|
|
1102
|
+
result = nil
|
|
1103
|
+
error = nil
|
|
1104
|
+
done = false
|
|
1105
|
+
worker = Thread.new do
|
|
1106
|
+
begin
|
|
1107
|
+
result = yield
|
|
1108
|
+
rescue StandardError => e
|
|
1109
|
+
error = e
|
|
1110
|
+
ensure
|
|
1111
|
+
done = true
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
until done
|
|
1116
|
+
QApplication.process_events
|
|
1117
|
+
sleep 0.05
|
|
1118
|
+
end
|
|
1119
|
+
worker.join
|
|
1120
|
+
raise error if error
|
|
1121
|
+
|
|
1122
|
+
result
|
|
1123
|
+
ensure
|
|
1124
|
+
if dialog
|
|
1125
|
+
dialog.close
|
|
1126
|
+
QApplication.process_events
|
|
1127
|
+
end
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
def install_progress_name(inspection)
|
|
1131
|
+
inspection&.metadata&.fetch("package", nil) || inspection&.display_name || "software"
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def install_progress_message_style
|
|
1135
|
+
<<~CSS
|
|
1136
|
+
QMessageBox {
|
|
1137
|
+
background-color: #ffffff;
|
|
1138
|
+
color: #15171a;
|
|
1139
|
+
border: 1px solid #cfd5dd;
|
|
1140
|
+
}
|
|
1141
|
+
QMessageBox QLabel {
|
|
1142
|
+
background-color: #ffffff;
|
|
1143
|
+
color: #15171a;
|
|
1144
|
+
font-size: 15px;
|
|
1145
|
+
font-weight: 700;
|
|
1146
|
+
padding: 10px;
|
|
1147
|
+
}
|
|
1148
|
+
QMessageBox QLabel#qt_msgbox_label {
|
|
1149
|
+
min-width: 340px;
|
|
1150
|
+
}
|
|
1151
|
+
QMessageBox QPushButton {
|
|
1152
|
+
background-color: #245fd6;
|
|
1153
|
+
color: #ffffff;
|
|
1154
|
+
}
|
|
1155
|
+
CSS
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
def dependency_names(depends)
|
|
1159
|
+
depends.to_s.split(",").map do |dependency|
|
|
1160
|
+
dependency.split("|").first.to_s.strip.sub(/\s*\(.+\)\z/, "")
|
|
1161
|
+
end.reject(&:empty?).uniq
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def dependency_summary(dependencies)
|
|
1165
|
+
shown = dependencies.first(6).join(", ")
|
|
1166
|
+
extra = dependencies.length - 6
|
|
1167
|
+
extra.positive? ? "#{dependencies.length} dependencies, including #{shown}, and #{extra} more" : shown
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
def rpm_requirement_names(requires)
|
|
1171
|
+
Array(requires).map do |requirement|
|
|
1172
|
+
requirement.to_s.sub(/\s*\(.+\)\z/, "")
|
|
1173
|
+
end.reject { |name| name.empty? || name.start_with?("rpmlib(") }.uniq
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
def rpm_version_label(metadata)
|
|
1177
|
+
[metadata["version"], metadata["release"]].compact.join("-").then { |value| value.empty? ? "unknown" : value }
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def active_icon_summary(manifest)
|
|
1181
|
+
custom = manifest["custom_icon"]
|
|
1182
|
+
return custom["path"] if custom && custom["path"].to_s != ""
|
|
1183
|
+
|
|
1184
|
+
manifest["default_icon_name"] || (manifest["icons"].to_a.any? ? manifest.fetch("app_id") : "none")
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
def warn_dialog(message)
|
|
1188
|
+
QMessageBox.warning(self, "Depot", message)
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
def unexpected_error(error)
|
|
1192
|
+
warn_dialog("Depot hit an interface error: #{error.message}")
|
|
1193
|
+
end
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1196
|
+
end
|