From 216852da1602f0eea9ed0cab5f641b24f0375f55 Mon Sep 17 00:00:00 2001 From: BoxBuddy Contributor Date: Mon, 17 Aug 2026 22:44:33 +0200 Subject: [PATCH 1/7] feat: add Start and Reboot container actions BoxBuddy previously only exposed Stop: a stopped container could not be brought back from the UI. This adds the two missing counterparts. start_box shells out to 'podman start NAME' via the same run_command helper that stop_box uses. distrobox start would also work, but it shells out to podman/docker itself; calling podman directly skips a layer of subprocess when the user just wants to wake a container up. The start button takes the place of stop in the titlebar trailing slot whenever the box is not running. reboot_box runs 'distrobox-stop NAME -Y; podman start NAME' in a terminal so the user sees both halves of the cycle. We pipe through bash -c the same way upgrade_box does, so the host's preferred terminal emulator is honoured and the user can interrupt. A new 'Reboot Box' row in the actions list invokes it. Always present, regardless of box state - rebooting a stopped box is equivalent to starting it, which is harmless. Both actions re-render the box list after the call so the new state (running/exited) shows up immediately in the notebook tab. --- src/distrobox_handler.rs | 63 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 41 +++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/distrobox_handler.rs b/src/distrobox_handler.rs index 22c6c82..1c7eb1c 100644 --- a/src/distrobox_handler.rs +++ b/src/distrobox_handler.rs @@ -688,6 +688,69 @@ pub fn stop_box(box_name: &str) { let _ = run_command("distrobox", Some(&["stop", box_name, "--yes"])); } +/// Starts a stopped container via the underlying container engine +/// (`podman start`). `distrobox start` would also work, but it shells +/// out to podman/docker itself, and calling it directly avoids one +/// extra layer of subprocess when all we want is to bring the +/// container back up. +pub fn start_box(box_name: &str) { + let _ = run_command("podman", Some(&["start", box_name])); +} + +/// Stops and then starts the box again so the user picks up any image +/// updates or in-box service restarts. Runs in a terminal so the user +/// can see the output. +pub fn reboot_box(box_name: &str) { + let (term, sep, term_is_flatpak) = get_terminal_and_separator_arg(); + let command = format!("distrobox-stop {box_name} -Y; podman start {box_name}"); + + if is_flatpak() { + if term_is_flatpak { + Command::new("flatpak-spawn") + .arg("--host") + .arg("flatpak") + .arg("run") + .arg(term) + .arg(sep) + .arg("bash") + .arg("-c") + .arg(&command) + .spawn() + .unwrap(); + } else { + Command::new("flatpak-spawn") + .arg("--host") + .arg(term) + .arg(sep) + .arg("bash") + .arg("-c") + .arg(&command) + .spawn() + .unwrap(); + } + } else { + if term_is_flatpak { + Command::new("flatpak") + .arg("run") + .arg(term) + .arg(sep) + .arg("bash") + .arg("-c") + .arg(&command) + .spawn() + .unwrap(); + } else { + Command::new(term) + .arg(sep) + .arg("bash") + .arg("-c") + .arg(&command) + .spawn() + .unwrap(); + } + } +} + /// Gets count of boxes, used to move the active page on the Notebook to the newest /// box after creation. pub fn get_number_of_boxes() -> u32 { diff --git a/src/main.rs b/src/main.rs index a20908a..39d8fff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,11 +17,11 @@ use gtk::{ mod distrobox_handler; use distrobox_handler::{ - assemble_box, clone_box, create_box, create_box_streaming, delete_box, export_app_from_box, get_all_distroboxes, - get_apps_in_box, get_available_images_with_distro_name, get_binaries_exported_from_box, + assemble_box, clone_box, create_box, create_box_streaming, delete_box, export_app_from_box, + get_all_distroboxes, get_apps_in_box, get_available_images_with_distro_name, get_binaries_exported_from_box, get_number_of_boxes, install_deb_in_box, install_rpm_in_box, open_terminal_in_box, - remove_app_from_host, remove_exported_binary_from_box, run_command_in_box, stop_box, - upgrade_all_boxes, upgrade_box, DBox, DBoxApp, + reboot_box, remove_app_from_host, remove_exported_binary_from_box, run_command_in_box, + start_box, stop_box, upgrade_all_boxes, upgrade_box, DBox, DBoxApp, }; mod utils; @@ -458,6 +458,20 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B delayed_rerender(&win_clone, Some(tab_num)); }); + // Start button is the visible counterpart of Stop - it makes sense to + // add the row right next to Stop so the titlebar keeps a single + // trailing button. + let start_btn = gtk::Button::from_icon_name("media-playback-start"); + // TRANSLATORS: Button tooltip + start_btn.set_tooltip_text(Some(&gettext("Start Box"))); + + let start_bn_clone = dbox.name.clone(); + let start_win_clone = window.clone(); + start_btn.connect_clicked(move |_btn| { + start_box(&start_bn_clone); + delayed_rerender(&start_win_clone, Some(tab_num)); + }); + let title_box = gtk::Box::new(Orientation::Horizontal, 10); title_box.set_margin_start(10); title_box.append(&page_img); @@ -466,6 +480,8 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B if dbox.is_running { title_box.append(&stop_btn); + } else { + title_box.append(&start_btn); } // list view @@ -499,6 +515,18 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B let up_bn_clone = box_name.clone(); upgrade_row.connect_activated(move |_row| on_upgrade_clicked(&up_bn_clone)); + // Reboot Box Icon + let reboot_icon = gtk::Image::from_icon_name("system-reboot-symbolic"); + + let reboot_row = ActionRow::new(); + // TRANSLATORS: Row Label + reboot_row.set_title(&gettext("Reboot Box")); + reboot_row.add_suffix(&reboot_icon); + reboot_row.set_activatable(true); + + let reboot_bn_clone = box_name.clone(); + reboot_row.connect_activated(move |_row| on_reboot_clicked(&reboot_bn_clone)); + // Show Applications Icon let show_applications_icon = gtk::Image::from_icon_name(&get_available_icon_name(APPLICATIONS_ICON_NAMES)); @@ -554,6 +582,7 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B // put all into list boxed_list.append(&open_terminal_row); boxed_list.append(&upgrade_row); + boxed_list.append(&reboot_row); boxed_list.append(&show_applications_row); // Make deb / rpm row if applicable @@ -1216,6 +1245,10 @@ fn build_empty_state_page(title: &str) -> adw::StatusPage { status_page } +fn on_reboot_clicked(box_name: &str) { + reboot_box(box_name); +} + fn on_show_applications_clicked(window: &ApplicationWindow, box_name: String) { let apps_popup = gtk::Window::builder() // TRANSLATORS: Window Title - shows list of installed applications in distrobox From 98c6d0c600dd16707c071977abd15cb47a93deaa Mon Sep 17 00:00:00 2001 From: Kacper Paczos Date: Wed, 19 Aug 2026 18:22:38 +0200 Subject: [PATCH 2/7] fix: start boxes through the detected runtime and current distrobox syntax start_box called podman by name, which breaks on docker-only hosts even though get_container_runtime exists for exactly this. reboot_box had the same hardcoded podman, and also spelled the stop as distrobox-stop - the split-binary form that distrobox v2 no longer installs; the stop subcommand works on both, and is what stop_box already uses. The Start button also asked for media-playback-start, which only exists in themes that kept the legacy names - Adwaita has dropped it, so the button was a broken-image placeholder on stock GNOME. The symbolic variant is bundled inside GTK itself and cannot go missing. --- src/distrobox_handler.rs | 17 +++++++++-------- src/main.rs | 10 +++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/distrobox_handler.rs b/src/distrobox_handler.rs index 1c7eb1c..d46728c 100644 --- a/src/distrobox_handler.rs +++ b/src/distrobox_handler.rs @@ -1,5 +1,5 @@ use crate::utils::{ - get_command_output, get_host_desktop_files, get_repository_list, + get_command_output, get_container_runtime, get_host_desktop_files, get_repository_list, get_terminal_and_separator_arg, is_flatpak, is_nvidia, run_command, }; use std::io::{BufRead, BufReader}; @@ -688,13 +688,13 @@ pub fn stop_box(box_name: &str) { let _ = run_command("distrobox", Some(&["stop", box_name, "--yes"])); } -/// Starts a stopped container via the underlying container engine -/// (`podman start`). `distrobox start` would also work, but it shells -/// out to podman/docker itself, and calling it directly avoids one -/// extra layer of subprocess when all we want is to bring the -/// container back up. +/// Starts a stopped container via the underlying container engine. +/// `distrobox start` does not exist; entering the box would start it too, +/// but that spawns a shell we would immediately have to throw away, so +/// asking the runtime directly is the quiet way to bring it back up. pub fn start_box(box_name: &str) { - let _ = run_command("podman", Some(&["start", box_name])); + let runtime = get_container_runtime(); + let _ = run_command(&runtime, Some(&["start", box_name])); } /// Stops and then starts the box again so the user picks up any image @@ -702,7 +702,8 @@ pub fn start_box(box_name: &str) { /// can see the output. pub fn reboot_box(box_name: &str) { let (term, sep, term_is_flatpak) = get_terminal_and_separator_arg(); - let command = format!("distrobox-stop {box_name} -Y; podman start {box_name}"); + let runtime = get_container_runtime(); + let command = format!("distrobox stop {box_name} --yes; {runtime} start {box_name}"); if is_flatpak() { if term_is_flatpak { diff --git a/src/main.rs b/src/main.rs index 39d8fff..bac06d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,10 +18,10 @@ use gtk::{ mod distrobox_handler; use distrobox_handler::{ assemble_box, clone_box, create_box, create_box_streaming, delete_box, export_app_from_box, - get_all_distroboxes, get_apps_in_box, get_available_images_with_distro_name, get_binaries_exported_from_box, - get_number_of_boxes, install_deb_in_box, install_rpm_in_box, open_terminal_in_box, - reboot_box, remove_app_from_host, remove_exported_binary_from_box, run_command_in_box, - start_box, stop_box, upgrade_all_boxes, upgrade_box, DBox, DBoxApp, + get_all_distroboxes, get_apps_in_box, get_available_images_with_distro_name, + get_binaries_exported_from_box, get_number_of_boxes, install_deb_in_box, install_rpm_in_box, + open_terminal_in_box, reboot_box, remove_app_from_host, remove_exported_binary_from_box, + run_command_in_box, start_box, stop_box, upgrade_all_boxes, upgrade_box, DBox, DBoxApp, }; mod utils; @@ -461,7 +461,7 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B // Start button is the visible counterpart of Stop - it makes sense to // add the row right next to Stop so the titlebar keeps a single // trailing button. - let start_btn = gtk::Button::from_icon_name("media-playback-start"); + let start_btn = gtk::Button::from_icon_name("media-playback-start-symbolic"); // TRANSLATORS: Button tooltip start_btn.set_tooltip_text(Some(&gettext("Start Box"))); From 0733d7a420cd7c65db63c625f0ad7b766d9542bb Mon Sep 17 00:00:00 2001 From: Kacper Paczos Date: Sat, 22 Aug 2026 23:35:08 +0200 Subject: [PATCH 3/7] feat: only offer Reboot Box while the box is running A stopped box has nothing to reboot - it is brought up with the Start button instead - so showing Reboot next to it is misleading. Add the Reboot row only when the box is running, matching how Start and Stop already appear by state. --- src/main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index bac06d4..d37b1d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -582,7 +582,11 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B // put all into list boxed_list.append(&open_terminal_row); boxed_list.append(&upgrade_row); - boxed_list.append(&reboot_row); + // Rebooting only makes sense for a box that is up; a stopped one is started + // with the Start button instead, so Reboot is offered only while running. + if dbox.is_running { + boxed_list.append(&reboot_row); + } boxed_list.append(&show_applications_row); // Make deb / rpm row if applicable From aec70c68ad27829895c0f428231a53a5fa0d7dc8 Mon Sep 17 00:00:00 2001 From: Kacper Paczos Date: Sun, 23 Aug 2026 09:47:02 +0200 Subject: [PATCH 4/7] simplify: reboot inline instead of through a terminal distrobox stop --yes and the runtime's start are both non-interactive, so Reboot now runs them directly - the same way Stop and Start already do - and refreshes the box's tab afterwards. That removes a copy of the four-way terminal spawn that the row did not need. --- src/distrobox_handler.rs | 56 +++------------------------------------- src/main.rs | 10 +++---- 2 files changed, 9 insertions(+), 57 deletions(-) diff --git a/src/distrobox_handler.rs b/src/distrobox_handler.rs index d46728c..5bdebf7 100644 --- a/src/distrobox_handler.rs +++ b/src/distrobox_handler.rs @@ -697,59 +697,11 @@ pub fn start_box(box_name: &str) { let _ = run_command(&runtime, Some(&["start", box_name])); } -/// Stops and then starts the box again so the user picks up any image -/// updates or in-box service restarts. Runs in a terminal so the user -/// can see the output. +/// Stops the box and brings it straight back up. Both halves are quiet, +/// non-interactive commands, so this runs inline like Stop and Start do. pub fn reboot_box(box_name: &str) { - let (term, sep, term_is_flatpak) = get_terminal_and_separator_arg(); - let runtime = get_container_runtime(); - let command = format!("distrobox stop {box_name} --yes; {runtime} start {box_name}"); - - if is_flatpak() { - if term_is_flatpak { - Command::new("flatpak-spawn") - .arg("--host") - .arg("flatpak") - .arg("run") - .arg(term) - .arg(sep) - .arg("bash") - .arg("-c") - .arg(&command) - .spawn() - .unwrap(); - } else { - Command::new("flatpak-spawn") - .arg("--host") - .arg(term) - .arg(sep) - .arg("bash") - .arg("-c") - .arg(&command) - .spawn() - .unwrap(); - } - } else { - if term_is_flatpak { - Command::new("flatpak") - .arg("run") - .arg(term) - .arg(sep) - .arg("bash") - .arg("-c") - .arg(&command) - .spawn() - .unwrap(); - } else { - Command::new(term) - .arg(sep) - .arg("bash") - .arg("-c") - .arg(&command) - .spawn() - .unwrap(); - } - } + stop_box(box_name); + start_box(box_name); } /// Gets count of boxes, used to move the active page on the Notebook to the newest diff --git a/src/main.rs b/src/main.rs index d37b1d3..a189fe0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -525,7 +525,11 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B reboot_row.set_activatable(true); let reboot_bn_clone = box_name.clone(); - reboot_row.connect_activated(move |_row| on_reboot_clicked(&reboot_bn_clone)); + let reboot_win_clone = window.clone(); + reboot_row.connect_activated(move |_row| { + reboot_box(&reboot_bn_clone); + delayed_rerender(&reboot_win_clone, Some(tab_num)); + }); // Show Applications Icon let show_applications_icon = @@ -1249,10 +1253,6 @@ fn build_empty_state_page(title: &str) -> adw::StatusPage { status_page } -fn on_reboot_clicked(box_name: &str) { - reboot_box(box_name); -} - fn on_show_applications_clicked(window: &ApplicationWindow, box_name: String) { let apps_popup = gtk::Window::builder() // TRANSLATORS: Window Title - shows list of installed applications in distrobox From 8121ae5fb2e42d081f8910c03d91d2cdcb6ec7b8 Mon Sep 17 00:00:00 2001 From: Kacper Paczos Date: Sun, 23 Aug 2026 19:11:33 +0200 Subject: [PATCH 5/7] feat: keep Start, Stop and Reboot in place, disabled when they do not apply Controls that come and go with the box's state make the page jump and hide what the box can do. Start and Stop now both sit in the header with the inapplicable one greyed out, and Reboot Box stays in the list on a stopped box, disabled and saying "Start the box first" - the same way Delete Box explains itself while the box is running. --- src/main.rs | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index a189fe0..80c7961 100644 --- a/src/main.rs +++ b/src/main.rs @@ -458,9 +458,7 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B delayed_rerender(&win_clone, Some(tab_num)); }); - // Start button is the visible counterpart of Stop - it makes sense to - // add the row right next to Stop so the titlebar keeps a single - // trailing button. + // Start is the counterpart of Stop and sits right next to it. let start_btn = gtk::Button::from_icon_name("media-playback-start-symbolic"); // TRANSLATORS: Button tooltip start_btn.set_tooltip_text(Some(&gettext("Start Box"))); @@ -478,11 +476,12 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B title_box.append(&page_title); title_box.append(&page_status); - if dbox.is_running { - title_box.append(&stop_btn); - } else { - title_box.append(&start_btn); - } + // Both buttons stay in place and the one that does not apply is disabled, + // so the header keeps its shape and the state is readable at a glance. + start_btn.set_sensitive(!dbox.is_running); + stop_btn.set_sensitive(dbox.is_running); + title_box.append(&start_btn); + title_box.append(&stop_btn); // list view let boxed_list = gtk::ListBox::new(); @@ -587,10 +586,14 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B boxed_list.append(&open_terminal_row); boxed_list.append(&upgrade_row); // Rebooting only makes sense for a box that is up; a stopped one is started - // with the Start button instead, so Reboot is offered only while running. - if dbox.is_running { - boxed_list.append(&reboot_row); + // with the Start button instead. The row stays put and is disabled, with + // the reason, rather than coming and going with the state. + if !dbox.is_running { + reboot_row.set_sensitive(false); + // TRANSLATORS: Explains why Reboot Box is greyed out on a stopped box + reboot_row.set_subtitle(&gettext("Start the box first")); } + boxed_list.append(&reboot_row); boxed_list.append(&show_applications_row); // Make deb / rpm row if applicable From 74d2ca36cfb6cfd7dc2bb0c4100ad47ea09b6947 Mon Sep 17 00:00:00 2001 From: Kacper Paczos Date: Sun, 23 Aug 2026 21:50:15 +0200 Subject: [PATCH 6/7] feat: grey out Reboot Box without a subtitle A one-line note under the row made it taller only while the box was stopped, so the list shifted on Start and Stop. The greyed-out row says enough on its own. --- src/main.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 80c7961..8d8ea78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -586,13 +586,9 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B boxed_list.append(&open_terminal_row); boxed_list.append(&upgrade_row); // Rebooting only makes sense for a box that is up; a stopped one is started - // with the Start button instead. The row stays put and is disabled, with - // the reason, rather than coming and going with the state. - if !dbox.is_running { - reboot_row.set_sensitive(false); - // TRANSLATORS: Explains why Reboot Box is greyed out on a stopped box - reboot_row.set_subtitle(&gettext("Start the box first")); - } + // with the Start button instead. The row stays put and is greyed out rather + // than coming and going with the state. + reboot_row.set_sensitive(dbox.is_running); boxed_list.append(&reboot_row); boxed_list.append(&show_applications_row); From d935c54f5eb223b14d51da81ae7298e4b228ceaa Mon Sep 17 00:00:00 2001 From: Kacper Paczos Date: Mon, 24 Aug 2026 10:49:18 +0200 Subject: [PATCH 7/7] feat: disable in-box actions while the box is stopped distrobox enter silently starts a stopped container, so Open Terminal, Upgrade, View Applications and the package-install row all acted as hidden Start buttons. Now that Start is an explicit header action, those rows are greyed out until the box is up - the state shown in the header and the state the rows act on can no longer disagree. --- src/main.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.rs b/src/main.rs index 8d8ea78..a954219 100644 --- a/src/main.rs +++ b/src/main.rs @@ -582,6 +582,13 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B let win_clone = window.clone(); clone_row.connect_activated(move |_row| on_clone_clicked(&win_clone, clone_bn.clone())); + // These rows run inside the container, and distrobox quietly starts a + // stopped box the moment one of them is used. Now that starting is an + // explicit action, they stay disabled until the box is actually up. + open_terminal_row.set_sensitive(dbox.is_running); + upgrade_row.set_sensitive(dbox.is_running); + show_applications_row.set_sensitive(dbox.is_running); + // put all into list boxed_list.append(&open_terminal_row); boxed_list.append(&upgrade_row); @@ -624,6 +631,9 @@ fn make_box_tab(dbox: &DBox, window: &ApplicationWindow, tab_num: u32) -> gtk::B boxed_list.append(&binary_row); } + // Installing a package also happens inside the container, so the row is + // gated like the other in-box actions above. + binary_row.set_sensitive(dbox.is_running); boxed_list.append(&clone_row); boxed_list.append(&delete_row);