Skip to content

Commit 2db6eb7

Browse files
committed
Add support for exporting binaries
1 parent cbbc012 commit 2db6eb7

File tree

5 files changed

+350
-31
lines changed

5 files changed

+350
-31
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "distroshelf"
33
version = "1.0.2"
4-
edition = "2021"
4+
edition = "2024"
55

66
[dependencies]
77
gettext-rs = { version = "0.7", features = ["gettext-system"] }

src/container.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ mod imp {
4040
pub distro: RefCell<Option<KnownDistro>>,
4141
#[property(get, set)]
4242
pub apps: RefCell<RemoteResource>,
43+
#[property(get, set)]
44+
pub binaries: RefCell<RemoteResource>,
4345
}
4446

4547
#[derived_properties]
@@ -79,7 +81,6 @@ impl Container {
7981

8082
let this_clone = this.clone();
8183
let loader = move |apps_list: Option<&gio::ListStore>| {
82-
dbg!(apps_list);
8384
let this = this_clone.clone();
8485
let mut apps_list = apps_list
8586
.cloned()
@@ -101,6 +102,29 @@ impl Container {
101102
};
102103
this.set_apps(RemoteResource::new::<gio::ListStore, _>(loader));
103104

105+
let this_clone = this.clone();
106+
let binaries_loader = move |binaries_list: Option<&gio::ListStore>| {
107+
let this = this_clone.clone();
108+
let mut binaries_list = binaries_list
109+
.cloned()
110+
.unwrap_or_else(gio::ListStore::new::<BoxedAnyObject>);
111+
async move {
112+
let binaries = this
113+
.root_store()
114+
.distrobox()
115+
.get_exported_binaries(&this.name())
116+
.await?;
117+
118+
binaries_list.remove_all();
119+
binaries_list.extend(binaries.into_iter().map(BoxedAnyObject::new));
120+
121+
// Listing the binaries starts the container, we need to update its status
122+
this.root_store().load_containers();
123+
Ok(binaries_list)
124+
}
125+
};
126+
this.set_binaries(RemoteResource::new::<gio::ListStore, _>(binaries_loader));
127+
104128
this
105129
}
106130

@@ -185,6 +209,32 @@ impl Container {
185209
Ok(())
186210
});
187211
}
212+
pub fn export_binary(&self, binary_path: &str) -> DistroboxTask {
213+
let this = self.clone();
214+
let binary_path = binary_path.to_string();
215+
self.root_store()
216+
.create_task(&self.name(), "export-binary", move |_task| async move {
217+
this.root_store()
218+
.distrobox()
219+
.export_binary(&this.name(), &binary_path)
220+
.await?;
221+
this.binaries().reload();
222+
Ok(())
223+
})
224+
}
225+
pub fn unexport_binary(&self, binary_path: &str) {
226+
let this = self.clone();
227+
let binary_path = binary_path.to_string();
228+
self.root_store()
229+
.create_task(&self.name(), "unexport-binary", move |_task| async move {
230+
this.root_store()
231+
.distrobox()
232+
.unexport_binary(&this.name(), &binary_path)
233+
.await?;
234+
this.binaries().reload();
235+
Ok(())
236+
});
237+
}
188238
pub fn clone_to(&self, target_name: &str) {
189239
let this = self.clone();
190240
let target_name_clone = target_name.to_string();

src/dialogs/create_distrobox_dialog.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ impl CreateDistroboxDialog {
500500
let imp = self.imp();
501501
imp.name_row.remove_css_class("error");
502502
imp.name_row.set_tooltip_text(None);
503-
if let Err(ref e) = res {
503+
if let Err(e) = res {
504504
error!(error = %e, "CreateDistroboxDialog: update_errors");
505505
}
506506
match res {

src/dialogs/exportable_apps_dialog.rs

Lines changed: 182 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use gtk::glib::{clone, BoxedAnyObject};
44
use gtk::{gio, glib, pango};
55

66
use crate::container::Container;
7-
use crate::distrobox::ExportableApp;
7+
use crate::distrobox::{ExportableApp, ExportableBinary};
8+
use crate::gtk_utils::reaction;
89

910
use std::cell::RefCell;
1011

@@ -20,19 +21,22 @@ mod imp {
2021
#[property(get, set)]
2122
pub container: RefCell<Container>,
2223
pub dialog: adw::Dialog,
24+
pub toast_overlay: adw::ToastOverlay,
2325
pub toolbar_view: adw::ToolbarView,
2426
pub content: gtk::Box,
2527
pub scrolled_window: gtk::ScrolledWindow,
2628
pub stack: gtk::Stack,
2729
pub error_label: gtk::Label,
2830
pub list_box: gtk::ListBox,
31+
pub binaries_list_box: gtk::ListBox,
32+
pub binary_name_entry: adw::EntryRow,
2933
}
3034

3135
#[derived_properties]
3236
impl ObjectImpl for ExportableAppsDialog {
3337
fn constructed(&self) {
3438
let obj = self.obj();
35-
obj.set_title("Exportable Apps");
39+
obj.set_title("Manage Exports");
3640
obj.set_content_width(360);
3741
obj.set_content_height(640);
3842

@@ -52,9 +56,9 @@ mod imp {
5256
self.stack.add_named(&self.error_label, Some("error"));
5357

5458
let loading_page = adw::StatusPage::new();
55-
loading_page.set_title("Loading App List");
59+
loading_page.set_title("Loading Exports");
5660
loading_page.set_description(Some(
57-
"Please wait while we load the list of exportable apps. This may take some time if the distrobox wasn't running",
61+
"Please wait while we load the list of exportable apps and binaries. This may take some time if the distrobox wasn't running",
5862
));
5963
loading_page.set_child(Some(&adw::Spinner::new()));
6064
self.stack.add_named(&loading_page, Some("loading"));
@@ -68,16 +72,41 @@ mod imp {
6872
export_apps_group.set_margin_bottom(12);
6973
export_apps_group.set_title("Exportable Apps");
7074
export_apps_group.add(&self.list_box);
71-
self.stack.add_named(&export_apps_group, Some("apps"));
75+
76+
// Setup binary export input
77+
self.binary_name_entry.set_title("Export New Binary");
78+
self.binary_name_entry.set_show_apply_button(true);
79+
self.binary_name_entry
80+
.add_css_class("add-binary-entry-row");
81+
82+
self.binaries_list_box.add_css_class("boxed-list");
83+
self.binaries_list_box.set_selection_mode(gtk::SelectionMode::None);
84+
self.binaries_list_box.set_margin_top(12);
85+
86+
let export_binaries_group = adw::PreferencesGroup::new();
87+
export_binaries_group.set_margin_start(12);
88+
export_binaries_group.set_margin_end(12);
89+
export_binaries_group.set_margin_top(0);
90+
export_binaries_group.set_margin_bottom(12);
91+
export_binaries_group.set_title("Exported Binaries");
92+
export_binaries_group.add(&self.binary_name_entry);
93+
export_binaries_group.add(&self.binaries_list_box);
94+
95+
let content_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
96+
content_box.append(&export_apps_group);
97+
content_box.append(&export_binaries_group);
98+
self.stack.add_named(&content_box, Some("apps"));
7299

73100
let empty_page = adw::StatusPage::new();
74-
empty_page.set_title("No Exportable Apps");
101+
empty_page.set_title("No Exportable Items");
102+
empty_page.set_description(Some("No applications or binaries found in this container"));
75103

76104
self.stack.add_named(&empty_page, Some("empty"));
77105

78106
self.content.append(&self.scrolled_window);
79107
self.toolbar_view.set_content(Some(&self.content));
80-
self.obj().set_child(Some(&self.toolbar_view));
108+
self.toast_overlay.set_child(Some(&self.toolbar_view));
109+
self.obj().set_child(Some(&self.toast_overlay));
81110
}
82111
}
83112

@@ -104,6 +133,22 @@ mod imp {
104133
this.container().unexport(file_path);
105134
},
106135
);
136+
klass.install_action(
137+
"dialog.export-binary",
138+
Some(VariantTy::STRING),
139+
|this, _action, target| {
140+
let binary_path = target.unwrap().str().unwrap();
141+
this.container().export_binary(binary_path);
142+
},
143+
);
144+
klass.install_action(
145+
"dialog.unexport-binary",
146+
Some(VariantTy::STRING),
147+
|this, _action, target| {
148+
let binary_path = target.unwrap().str().unwrap();
149+
this.container().unexport_binary(binary_path);
150+
},
151+
);
107152
}
108153
}
109154

@@ -121,43 +166,119 @@ impl ExportableAppsDialog {
121166
.property("container", container)
122167
.build();
123168

169+
let apps = this.container().apps();
170+
let binaries = this.container().binaries();
171+
172+
let is_empty = move || -> bool {
173+
let n_apps = apps.data::<gio::ListStore>().map(|s| s.n_items()).unwrap_or(0);
174+
let n_binaries = binaries.data::<gio::ListStore>().map(|s| s.n_items()).unwrap_or(0);
175+
n_apps == 0 && n_binaries == 0
176+
};
177+
124178
let this_clone = this.clone();
125-
container.apps().connect_loading_notify(move |resource| {
126-
if resource.loading() {
127-
this_clone.imp().stack.set_visible_child_name("loading");
128-
}
129-
});
130-
let this_clone = this.clone();
131-
container.apps().connect_error_notify(move |resource| {
132-
if let Some(err) = resource.error() {
133-
this_clone.imp().error_label.set_label(&err);
134-
this_clone.imp().stack.set_visible_child_name("error");
179+
let apps = this.container().apps();
180+
let binaries = this.container().binaries();
181+
reaction! {
182+
(apps.error(), binaries.error()),
183+
move |(e1, e2): (Option<String>, Option<String>)| {
184+
if let Some(err) = e1.or(e2) {
185+
this_clone.imp().error_label.set_label(&err);
186+
this_clone.imp().stack.set_visible_child_name("error");
187+
}
135188
}
136-
});
189+
};
190+
137191
let this_clone = this.clone();
138-
container.apps().connect_data_changed(move |resource| {
139-
let apps = resource.data::<gio::ListStore>().unwrap();
140-
141-
if apps.n_items() == 0 {
142-
this_clone.imp().stack.set_visible_child_name("empty");
143-
return;
144-
}
192+
let apps = this.container().apps();
193+
let render_apps = move || {
194+
let apps = apps.data::<gio::ListStore>();
145195

146196
this_clone.imp().stack.set_visible_child_name("apps");
147-
148197
let this = this_clone.clone();
149198
this_clone
150199
.imp()
151200
.list_box
152-
.bind_model(Some(&apps), move |obj| {
201+
.bind_model(apps.as_ref(), move |obj| {
153202
let app = obj
154203
.downcast_ref::<BoxedAnyObject>()
155204
.map(|obj| obj.borrow::<ExportableApp>())
156205
.unwrap();
157206
this.build_row(&app).upcast()
158207
});
159-
});
208+
209+
};
210+
211+
let this_clone = this.clone();
212+
let binaries = this.container().binaries();
213+
let render_binaries = move || {
214+
let binaries = binaries.data::<gio::ListStore>();
215+
216+
this_clone.imp().stack.set_visible_child_name("apps");
217+
let this = this_clone.clone();
218+
this_clone
219+
.imp()
220+
.binaries_list_box
221+
.bind_model(binaries.as_ref(), move |obj| {
222+
let binary = obj
223+
.downcast_ref::<BoxedAnyObject>()
224+
.map(|obj| obj.borrow::<ExportableBinary>())
225+
.unwrap();
226+
this.build_binary_row(&binary).upcast()
227+
});
228+
};
229+
230+
let this_clone = this.clone();
231+
let apps = this.container().apps();
232+
let binaries = this.container().binaries();
233+
reaction! {
234+
(apps.loading(), binaries.loading()),
235+
move |(b1, b2): (bool, bool)| {
236+
if b1 || b2 {
237+
this_clone.imp().stack.set_visible_child_name("loading");
238+
} else if is_empty() {
239+
this_clone.imp().stack.set_visible_child_name("empty");
240+
} else {
241+
render_apps();
242+
render_binaries();
243+
}
244+
}
245+
};
246+
247+
248+
// Connect the binary name entry apply signal
249+
let this_clone = this.clone();
250+
this.imp()
251+
.binary_name_entry
252+
.connect_apply(move |entry| {
253+
let binary_name = entry.text().to_string();
254+
if !binary_name.is_empty() {
255+
let task = this_clone.container().export_binary(&binary_name);
256+
entry.set_text("");
257+
258+
// Monitor task status to show error toasts
259+
let this = this_clone.clone();
260+
let binary_name_clone = binary_name.clone();
261+
reaction!(task.status(), move |status: String| {
262+
match status.as_str() {
263+
"failed" => {
264+
let error_ref = task.error();
265+
let error_msg = if let Some(err) = error_ref.as_ref() {
266+
format!("Failed to export '{}': {}", binary_name_clone, err)
267+
} else {
268+
format!("Failed to export '{}'", binary_name_clone)
269+
};
270+
let toast = adw::Toast::new(&error_msg);
271+
toast.set_timeout(5);
272+
this.imp().toast_overlay.add_toast(toast);
273+
}
274+
_ => {}
275+
}
276+
});
277+
}
278+
});
279+
160280
container.apps().reload();
281+
container.binaries().reload();
161282

162283
this
163284
}
@@ -212,4 +333,37 @@ impl ExportableAppsDialog {
212333

213334
row
214335
}
336+
337+
pub fn build_binary_row(&self, binary: &ExportableBinary) -> adw::ActionRow {
338+
// Create the action row
339+
let row = adw::ActionRow::new();
340+
row.set_title(&binary.name);
341+
row.set_subtitle(&binary.source_path);
342+
343+
// Create the menu button
344+
let menu_button = gtk::MenuButton::new();
345+
menu_button.set_icon_name("view-more-symbolic");
346+
menu_button.set_valign(gtk::Align::Center);
347+
menu_button.add_css_class("flat");
348+
349+
// Create the menu model - only show unexport since we're only showing exported binaries
350+
let menu_model = gio::Menu::new();
351+
let unexport_action = gio::MenuItem::new(
352+
Some("Unexport Binary"),
353+
Some(&format!(
354+
"dialog.unexport-binary(\"{}\")",
355+
binary.source_path
356+
)),
357+
);
358+
menu_model.append_item(&unexport_action);
359+
360+
// Set up the popover menu
361+
let popover = gtk::PopoverMenu::from_model(Some(&menu_model));
362+
menu_button.set_popover(Some(&popover));
363+
364+
// Add the menu button to the action row
365+
row.add_suffix(&menu_button);
366+
367+
row
368+
}
215369
}

0 commit comments

Comments
 (0)