Skip to content

Commit 1026dee

Browse files
committed
Clone container ui
1 parent a071855 commit 1026dee

10 files changed

Lines changed: 157 additions & 131 deletions

File tree

src/application.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ use std::rc::Rc;
2323

2424
use adw::prelude::*;
2525
use adw::subclass::prelude::*;
26-
use gettextrs::gettext;
2726
use gtk::{gio, glib};
2827

29-
use crate::config::{self, VERSION};
28+
use crate::config;
3029
use crate::distrobox::{Distrobox, DistroboxCommandRunnerResponse, FlatpakCommandRunner};
3130
use crate::fakers::{CommandRunner, RealCommandRunner};
3231
use crate::root_store::RootStore;

src/container.rs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
distrobox::{ContainerInfo, ExportableApp, Status},
2+
distrobox::{ContainerInfo, CreateArgs, CreateArgName, ExportableApp, Status},
33
distrobox_task::DistroboxTask,
44
fakers::CommandRunner,
55
gtk_utils::TypedListStore,
@@ -273,22 +273,6 @@ impl Container {
273273
Ok(())
274274
});
275275
}
276-
pub fn clone_to(&self, target_name: &str) {
277-
let this = self.clone();
278-
let target_name_clone = target_name.to_string();
279-
let task = self
280-
.root_store()
281-
.create_task(&this.name(), "clone", move |task| async move {
282-
let child = this
283-
.root_store()
284-
.distrobox()
285-
.clone_to(&this.name(), &target_name_clone)
286-
.await?;
287-
task.handle_child_output(child).await?;
288-
Ok(())
289-
});
290-
self.root_store().view_task(&task);
291-
}
292276
pub fn delete(&self) {
293277
let this = self.clone();
294278
self.root_store()

src/dialogs/create_distrobox_dialog.rs

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use tracing::error;
66

77
use crate::distrobox::{self, CreateArgName, CreateArgs, Error};
88
use crate::root_store::RootStore;
9+
use crate::container::Container;
10+
use crate::sidebar_row::SidebarRow;
911

1012
use std::path::PathBuf;
1113
use std::{cell::RefCell, rc::Rc};
@@ -42,6 +44,38 @@ mod imp {
4244
pub init_row: adw::SwitchRow,
4345
pub volume_rows: Rc<RefCell<Vec<adw::EntryRow>>>,
4446
pub scrolled_window: gtk::ScrolledWindow,
47+
#[property(get, set=Self::set_clone_src, nullable)]
48+
pub clone_src: RefCell<Option<Container>>,
49+
// transient widget used to show the source container info when cloning
50+
pub clone_sidebar: RefCell<Option<SidebarRow>>,
51+
pub cloning_content: gtk::Box,
52+
pub view_switcher: adw::InlineViewSwitcher,
53+
}
54+
55+
impl CreateDistroboxDialog {
56+
fn set_clone_src(&self, value: Option<Container>) {
57+
// store the value
58+
self.clone_src.replace(value.clone());
59+
60+
if let Some(sidebar_row) = self.clone_sidebar.borrow_mut().take() {
61+
self.cloning_content.remove(&sidebar_row);
62+
}
63+
64+
if let Some(container) = value {
65+
self.image_row.set_visible(false);
66+
self.cloning_content.set_visible(true);
67+
self.view_switcher.set_visible(false);
68+
let sidebar_row = SidebarRow::new(&container);
69+
// insert at the top of the cloning_content box
70+
self.cloning_content.append(&sidebar_row);
71+
self.clone_sidebar.replace(Some(sidebar_row));
72+
} else {
73+
// no clone source, ensure image row is visible
74+
self.image_row.set_visible(true);
75+
self.cloning_content.set_visible(false);
76+
self.view_switcher.set_visible(true);
77+
}
78+
}
4579
}
4680

4781
#[derived_properties]
@@ -56,15 +90,35 @@ mod imp {
5690
// Create view switcher and stack
5791
let view_stack = adw::ViewStack::new();
5892

59-
// Create GUI creation page
60-
let gui_page = gtk::Box::new(gtk::Orientation::Vertical, 12);
61-
gui_page.set_margin_start(12);
62-
gui_page.set_margin_end(12);
63-
gui_page.set_margin_top(12);
64-
gui_page.set_margin_bottom(12);
93+
self.content.set_margin_start(12);
94+
self.content.set_margin_end(12);
95+
self.content.set_margin_top(12);
96+
self.content.set_margin_bottom(12);
97+
self.content.set_spacing(12);
98+
self.content.set_orientation(gtk::Orientation::Vertical);
99+
100+
101+
// Create cloning_content box with header and sidebar
102+
self.cloning_content.set_orientation(gtk::Orientation::Vertical);
103+
self.cloning_content.set_spacing(12);
104+
self.cloning_content.set_visible(false);
105+
106+
// Create header box with "Cloning" label
107+
let cloning_header = gtk::Box::new(gtk::Orientation::Horizontal, 12);
108+
cloning_header.set_homogeneous(false);
109+
110+
let cloning_label = gtk::Label::new(Some("Cloning"));
111+
cloning_label.set_halign(gtk::Align::Start);
112+
cloning_label.add_css_class("title-3");
113+
114+
cloning_header.set_hexpand(true);
115+
cloning_header.append(&cloning_label);
116+
117+
self.cloning_content.append(&cloning_header);
118+
self.content.append(&self.cloning_content);
119+
65120
let preferences_group = adw::PreferencesGroup::new();
66121
preferences_group.set_title("Settings");
67-
68122
self.name_row.set_title("Name");
69123

70124
self.image_row
@@ -133,8 +187,8 @@ mod imp {
133187
preferences_group.add(&self.init_row);
134188

135189
let volumes_group = self.obj().build_volumes_group();
136-
gui_page.append(&preferences_group);
137-
gui_page.append(&volumes_group);
190+
self.content.append(&preferences_group);
191+
self.content.append(&volumes_group);
138192

139193
let create_btn = gtk::Button::with_label("Create");
140194
create_btn.set_halign(gtk::Align::Center);
@@ -148,7 +202,14 @@ mod imp {
148202
let res = obj.extract_create_args().await;
149203
obj.update_errors(&res);
150204
if let Ok(create_args) = res {
151-
obj.root_store().create_container(create_args);
205+
// If cloning from a source, delegate to clone_container, otherwise create normally
206+
if let Some(src) = obj.clone_src() {
207+
obj.root_store()
208+
.clone_container(&src.name(), create_args);
209+
} else {
210+
obj.root_store().create_container(create_args);
211+
}
212+
obj.close();
152213
}
153214
});
154215
}
@@ -157,7 +218,7 @@ mod imp {
157218
create_btn.add_css_class("pill");
158219
create_btn.set_margin_top(12);
159220

160-
gui_page.append(&create_btn);
221+
self.content.append(&create_btn);
161222

162223
// Create page for assemble from file
163224
let assemble_page = gtk::Box::new(gtk::Orientation::Vertical, 12);
@@ -261,22 +322,22 @@ mod imp {
261322
});
262323

263324
// Add pages to view stack
264-
view_stack.add_titled(&gui_page, Some("create"), "Guided");
325+
view_stack.add_titled(&self.content, Some("create"), "Guided");
265326
view_stack.add_titled(&assemble_page, Some("assemble-file"), "From File");
266327
view_stack.add_titled(&url_page, Some("assemble-url"), "From URL");
267328

329+
268330
// Create a box to hold the view switcher and content
269331
let content_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
270332

271333
// Add inline view switcher
272-
let view_switcher = adw::InlineViewSwitcher::new();
273-
view_switcher.set_stack(Some(&view_stack));
274-
view_switcher.set_margin_start(12);
275-
view_switcher.set_margin_end(12);
276-
view_switcher.set_margin_top(12);
277-
view_switcher.set_margin_bottom(12);
278-
279-
content_box.append(&view_switcher);
334+
self.view_switcher.set_stack(Some(&view_stack));
335+
self.view_switcher.set_margin_start(12);
336+
self.view_switcher.set_margin_end(12);
337+
self.view_switcher.set_margin_top(12);
338+
self.view_switcher.set_margin_bottom(12);
339+
340+
content_box.append(&self.view_switcher);
280341
content_box.append(&view_stack);
281342

282343
// Wrap content_box in a scrolled window

src/dialogs/preferences_dialog.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::terminal_combo_row::TerminalComboRow;
44
use adw::prelude::*;
55
use adw::subclass::prelude::*;
66
use glib::{clone, derived_properties, Properties};
7-
use gtk::{gio, glib};
7+
use gtk::glib;
88
use std::cell::RefCell;
99
use tracing::error;
1010

src/distrobox/mod.rs

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,10 @@ impl Distrobox {
672672
}
673673

674674
/// Lists only the binaries that have already been exported from the container.
675-
pub async fn get_exported_binaries(&self, box_name: &str) -> Result<Vec<ExportableBinary>, Error> {
675+
pub async fn get_exported_binaries(
676+
&self,
677+
box_name: &str,
678+
) -> Result<Vec<ExportableBinary>, Error> {
676679
let mut cmd = dbcmd();
677680
cmd.args([
678681
"enter",
@@ -684,33 +687,33 @@ impl Distrobox {
684687
// Example output: '/usr/bin/vim' | /home/user/.local/bin/vim
685688
let output = self.cmd_output_string(cmd).await?;
686689
debug!(binaries_output = output);
687-
690+
688691
let mut binaries = Vec::new();
689692
for line in output.lines() {
690693
if line.is_empty() || !line.contains('|') {
691694
continue;
692695
}
693-
696+
694697
let parts: Vec<&str> = line.split('|').collect();
695698
if parts.len() >= 2 {
696699
let source_path = parts[0].trim().to_string();
697700
// For some reason distrobox formats the source path between single quotes, so we need to remove those
698701
let source_path = source_path.trim_matches('\'').to_string();
699702

700703
let exported_path_str = parts[1].trim();
701-
704+
702705
// Only include binaries that have a non-empty exported path. It should always be the case, but BoxBuddy defensively checks it.
703706
// In this case we try to follow BoxBuddy's behavior to keep consistency for users.
704707
if !exported_path_str.is_empty() {
705708
let exported_path = exported_path_str.to_string();
706-
709+
707710
// Extract binary name from source path
708711
let name = Path::new(&source_path)
709712
.file_name()
710713
.and_then(|n| n.to_str())
711714
.unwrap_or(&source_path)
712715
.to_string();
713-
716+
714717
binaries.push(ExportableBinary {
715718
name,
716719
source_path,
@@ -719,7 +722,7 @@ impl Distrobox {
719722
}
720723
}
721724
}
722-
725+
723726
Ok(binaries)
724727
}
725728

@@ -774,7 +777,8 @@ impl Distrobox {
774777
// If it doesn't contain a '/' it's likely just a binary name
775778
let resolved_path = if !binary_name_or_path.contains('/') {
776779
// Resolve the binary name to its full path using 'which'
777-
self.resolve_binary_path(container, binary_name_or_path).await?
780+
self.resolve_binary_path(container, binary_name_or_path)
781+
.await?
778782
} else {
779783
binary_name_or_path.to_string()
780784
};
@@ -789,21 +793,25 @@ impl Distrobox {
789793
}
790794

791795
/// Resolves a binary name to its full path using 'which' inside the container
792-
async fn resolve_binary_path(&self, container: &str, binary_name: &str) -> Result<String, Error> {
796+
async fn resolve_binary_path(
797+
&self,
798+
container: &str,
799+
binary_name: &str,
800+
) -> Result<String, Error> {
793801
let mut cmd = dbcmd();
794802
cmd.args(["enter", "--name", container, "--", "which", binary_name]);
795-
803+
796804
let output = self.cmd_output_string(cmd).await?;
797805
let path = output.trim();
798-
806+
799807
if path.is_empty() {
800808
return Err(Error::CommandFailed {
801809
exit_code: Some(1),
802810
command: format!("which {}", binary_name),
803811
stderr: format!("Binary '{}' not found in container", binary_name),
804812
});
805813
}
806-
814+
807815
Ok(path.to_string())
808816
}
809817

@@ -848,8 +856,7 @@ impl Distrobox {
848856
cmd.arg("assemble").arg("create").arg("--file").arg(url);
849857
self.cmd_spawn(cmd)
850858
}
851-
// create
852-
pub async fn create(&self, args: CreateArgs) -> Result<Box<dyn Child + Send>, Error> {
859+
fn create_cmd(args: CreateArgs) -> Command {
853860
let mut cmd = dbcmd();
854861
cmd.arg("create").arg("--yes");
855862
if !args.image.is_empty() {
@@ -872,6 +879,11 @@ impl Distrobox {
872879
for volume in args.volumes {
873880
cmd.arg("--volume").arg(volume.to_string());
874881
}
882+
cmd
883+
}
884+
// create
885+
pub async fn create(&self, args: CreateArgs) -> Result<Box<dyn Child + Send>, Error> {
886+
let cmd = Self::create_cmd(args);
875887
self.cmd_spawn(cmd)
876888
}
877889
// create --compatibility
@@ -897,18 +909,16 @@ impl Distrobox {
897909
cmd.arg("enter").arg(name);
898910
cmd
899911
}
900-
// clone
901-
pub async fn clone_to(
912+
// clone from an existing container using create args to customize the clone
913+
pub async fn clone_from(
902914
&self,
903915
source_name: &str,
904-
target_name: &str,
916+
args: CreateArgs,
905917
) -> Result<Box<dyn Child + Send>, Error> {
906-
let mut cmd = dbcmd();
907-
cmd.arg("create")
908-
.arg("--name")
909-
.arg(target_name)
910-
.arg("--clone")
911-
.arg(source_name);
918+
let mut cmd = Self::create_cmd(args);
919+
cmd.remove_flag_value_arg("--image");
920+
cmd.remove_flag_arg("--yes");
921+
cmd.arg("--clone").arg(source_name);
912922
self.cmd_spawn(cmd)
913923
}
914924
// list | ls

src/fakers/command.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{
2-
ffi::{OsStr, OsString}, fmt::Display, process::Stdio
2+
ffi::{OsStr, OsString},
3+
fmt::Display,
4+
process::Stdio,
35
};
46

57
#[derive(Debug, Clone)]
@@ -85,6 +87,21 @@ impl Command {
8587
self.args.push(arg.as_ref().to_owned());
8688
self
8789
}
90+
91+
// removes the first occurrence of an arg by name and its value
92+
pub fn remove_flag_value_arg(&mut self, name: &str) -> &mut Command {
93+
self.args.iter().position(|x| x == name).map(|index| {
94+
self.args.remove(index);
95+
self.args.remove(index); // same index, as the vector has shifted
96+
});
97+
self
98+
}
99+
pub fn remove_flag_arg(&mut self, name: &str) -> &mut Command {
100+
self.args.iter().position(|x| x == name).map(|index| {
101+
self.args.remove(index);
102+
});
103+
self
104+
}
88105
}
89106

90107
impl Display for Command {

0 commit comments

Comments
 (0)