├── po ├── meson.build └── LINGUAS ├── .rustfmt.toml ├── data ├── screenshots │ ├── dark.png │ └── light.png ├── resources │ ├── style-hc-dark.css │ ├── icons │ │ └── scalable │ │ │ ├── actions │ │ │ ├── funnel-symbolic.svg │ │ │ ├── get-symbolic.svg │ │ │ ├── put-symbolic.svg │ │ │ ├── arrow1-right-symbolic.svg │ │ │ ├── regex-symbolic.svg │ │ │ ├── memory-symbolic.svg │ │ │ ├── merge-symbolic.svg │ │ │ ├── pods-symbolic.svg │ │ │ ├── code-symbolic.svg │ │ │ ├── skull-symbolic.svg │ │ │ ├── processor-symbolic.svg │ │ │ ├── uppercase-symbolic.svg │ │ │ ├── about-symbolic.svg │ │ │ ├── success-small-symbolic.svg │ │ │ ├── puzzle-piece-symbolic.svg │ │ │ ├── stacked-plates-symbolic.svg │ │ │ ├── cross-symbolic.svg │ │ │ ├── whole-word-symbolic.svg │ │ │ ├── bell-outline-symbolic.svg │ │ │ ├── city-symbolic.svg │ │ │ ├── eraser5-symbolic.svg │ │ │ ├── build-configure-symbolic.svg │ │ │ ├── ambulance-symbolic.svg │ │ │ └── pip-out-symbolic.svg │ │ │ └── status │ │ │ ├── error-symbolic.svg │ │ │ ├── success-symbolic.svg │ │ │ ├── local-connection-symbolic.svg │ │ │ └── verified-checkmark-symbolic.svg │ ├── meson.build │ ├── style-dark.css │ ├── style-hc.css │ └── resources.gresource.xml ├── resources.gresource.xml.in ├── icons │ ├── meson.build │ └── com.github.marhkb.Pods-symbolic.svg ├── com.github.marhkb.Pods.desktop.in.in └── meson.build ├── .github ├── ISSUE_TEMPLATE │ ├── blank_issue.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── issues.yml ├── src ├── podman.rs ├── widget │ ├── property_widget_row.ui │ ├── spinner.ui │ ├── circular_progress_bar.ui │ ├── random_name_entry_row.ui │ ├── mod.rs │ ├── main_menu_button.ui │ ├── property_row.ui │ ├── main_menu_button.rs │ ├── zoom_control.ui │ ├── source_view_search_widget.ui │ ├── count_badge.ui │ ├── text_search_entry.ui │ ├── random_name_entry_row.rs │ └── scalable_text_view.rs ├── config.rs.in ├── view │ ├── value_row.ui │ ├── container_renamer.ui │ ├── repo_tag_simple_row.ui │ ├── top_page_action_bar.ui │ ├── containers_list_view.ui │ ├── volumes_group.ui │ ├── containers_group.ui │ ├── actions_button.ui │ ├── image_menu_button.ui │ ├── containers_grid_view.ui │ ├── image_pull_page.ui │ ├── image_selection_combo_row.ui │ ├── connection.rs │ ├── container_health_check_log_row.ui │ ├── key_val_row.ui │ ├── info_row.ui │ ├── pods_row.rs │ ├── images_row.rs │ ├── image_history_page.ui │ ├── volumes_row.rs │ ├── welcome_page.rs │ ├── containers_row.rs │ ├── top_page.ui │ ├── action_row.ui │ ├── container_volume_row.ui │ ├── repo_tag_row.ui │ ├── pod_menu_button.ui │ ├── window.ui │ ├── volume.rs │ ├── images_row.ui │ ├── volumes_row.ui │ ├── container_menu_button.ui │ ├── repo_tag_add_dialog.ui │ ├── containers_count_bar.ui │ ├── container_terminal_page.ui │ ├── device_row.ui │ ├── pods_prune_page.ui │ ├── connections_sidebar.ui │ ├── pods_prune_page.rs │ ├── image.rs │ ├── info_row.rs │ ├── pods_row.ui │ ├── image_search_response_row.ui │ ├── containers_row.ui │ ├── volume_creation_page.ui │ ├── image_pull_page.rs │ ├── containers_prune_page.ui │ ├── volumes_prune_page.ui │ ├── containers_list_view.rs │ ├── container_renamer.rs │ ├── volumes_prune_page.rs │ └── containers_prune_page.rs ├── model │ ├── pod_data.rs │ ├── selectable.rs │ ├── container_volume_list.rs │ ├── value.rs │ ├── health_check_log.rs │ ├── key_val.rs │ ├── image_data.rs │ ├── container_volume.rs │ ├── repo_tag.rs │ ├── health_check_log_list.rs │ ├── image_config.rs │ ├── device.rs │ ├── image_search_response.rs │ └── port_mapping_list.rs └── meson.build ├── .gitignore ├── meson_options.txt ├── .typos.toml ├── .editorconfig ├── Pods.doap ├── Cargo.toml ├── hooks └── pre-commit.hook └── meson.build /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext(gettext_package, preset: 'glib') 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Item" 2 | group_imports = "StdExternalCrate" 3 | -------------------------------------------------------------------------------- /data/screenshots/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhkb/pods/HEAD/data/screenshots/dark.png -------------------------------------------------------------------------------- /data/screenshots/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marhkb/pods/HEAD/data/screenshots/light.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank Issue 3 | about: Create a blank issue. 4 | --- 5 | -------------------------------------------------------------------------------- /src/podman.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use podman_api::api; 2 | pub(crate) use podman_api::models; 3 | pub(crate) use podman_api::opts; 4 | pub(crate) use podman_api::*; 5 | -------------------------------------------------------------------------------- /data/resources/style-hc-dark.css: -------------------------------------------------------------------------------- 1 | containercard separator { 2 | background-color: var(--border-color); 3 | } 4 | 5 | containercard.not-running { 6 | background-color: transparent; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | build/ 3 | _build/ 4 | builddir/ 5 | build-aux/app 6 | **/.flatpak-builder/ 7 | src/config.rs 8 | *.ui.in~ 9 | *.ui~ 10 | .flatpak/ 11 | vendor 12 | .vscode 13 | __pycache__/ 14 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- 1 | ar 2 | ca 3 | da 4 | de 5 | es 6 | et 7 | eu 8 | fa 9 | fi 10 | fr 11 | he 12 | id 13 | it 14 | nb_NO 15 | nl 16 | oc 17 | pl 18 | pt 19 | pt_BR 20 | ro 21 | ru 22 | si 23 | sv 24 | ta 25 | tr 26 | uk 27 | zh_Hans 28 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/funnel-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources.gresource.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @app-id@.metainfo.xml 5 | 6 | 7 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'profile', 3 | type: 'combo', 4 | choices: [ 5 | 'default', 6 | 'development' 7 | ], 8 | value: 'default', 9 | description: 'The build profile for Pods. One of "default" or "development".' 10 | ) 11 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [type.po] 2 | extend-glob = ["*.po"] 3 | check-file = false 4 | 5 | [type.svg] 6 | extend-glob = ["*.svg"] 7 | check-file = false 8 | 9 | [default] 10 | extend-ignore-re = ["(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on"] 11 | -------------------------------------------------------------------------------- /data/resources/meson.build: -------------------------------------------------------------------------------- 1 | # Resources 2 | resources = gnome.compile_resources( 3 | 'resources', 4 | 'resources.gresource.xml', 5 | gresource_bundle: true, 6 | source_dir: meson.current_build_dir(), 7 | install: true, 8 | install_dir: pkgdatadir, 9 | ) 10 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/status/error-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/get-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/put-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/arrow1-right-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | '@0@.svg'.format(application_id), 3 | install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps' 4 | ) 5 | 6 | install_data( 7 | '@0@-symbolic.svg'.format(base_id), 8 | install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps', 9 | rename: '@0@-symbolic.svg'.format(application_id) 10 | ) 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or improvement 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | 11 | 12 | #### Describe your feature request 13 | 14 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/regex-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/icons/com.github.marhkb.Pods-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/memory-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/merge-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/pods-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/status/success-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/code-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | charset = utf-8 8 | 9 | [*.{build,css,doap,scss,ui,xml,xml.in,xml.in.in,yaml,yml}] 10 | indent_size = 2 11 | 12 | [*.{json,py,rs}] 13 | indent_size = 4 14 | 15 | [*.{c,h,h.in}] 16 | indent_size = 2 17 | max_line_length = 80 18 | 19 | [NEWS] 20 | indent_size = 2 21 | max_line_length = 72 22 | -------------------------------------------------------------------------------- /data/com.github.marhkb.Pods.desktop.in.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Pods 3 | Comment=Manage your Podman containers 4 | Type=Application 5 | Exec=pods 6 | Terminal=false 7 | Categories=GNOME;GTK;Development;System;Office;Network;Monitor;TerminalEmulator;RemoteAccess; 8 | Keywords=Gnome;GTK;libadwaita;Podman;Containerization; 9 | # Translators: Do NOT translate or transliterate this text (this is an icon file name)! 10 | Icon=@icon@ 11 | StartupNotify=true 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 12 | 13 | ### Reproduction steps 14 | 15 | ### Environment 16 | 17 | - Pods version: 18 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/skull-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/style-dark.css: -------------------------------------------------------------------------------- 1 | containercard separator { 2 | background-color: var(--window-bg-color); 3 | } 4 | 5 | actionsbutton.finished .action-count-badge { 6 | background-color: var(--light-3); 7 | color: var(--dark-5); 8 | } 9 | 10 | .version, 11 | .container-health-status-not-running, 12 | .container-status-not-running, 13 | .image-unused, 14 | .pod-status-not-running, 15 | .volume-unused, 16 | .rich-expander-row-header #badge { 17 | color: var(--dark-3); 18 | background-color: var(--light-5); 19 | } 20 | -------------------------------------------------------------------------------- /src/widget/property_widget_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/processor-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/widget/spinner.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/uppercase-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/widget/circular_progress_bar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /src/config.rs.in: -------------------------------------------------------------------------------- 1 | pub(crate) const APP_ID: &str = @APP_ID@; 2 | pub(crate) const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; 3 | pub(crate) const LOCALEDIR: &str = @LOCALEDIR@; 4 | pub(crate) const PKGDATADIR: &str = @PKGDATADIR@; 5 | pub(crate) const PROFILE: &str = @PROFILE@; 6 | pub(crate) const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); 7 | pub(crate) const APPDATA_RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/appdata-resources.gresource"); 8 | pub(crate) const UI_RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/ui-resources.gresource"); 9 | pub(crate) const VERSION: &str = @VERSION@; 10 | -------------------------------------------------------------------------------- /src/widget/random_name_entry_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /src/view/value_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/about-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/success-small-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/view/container_renamer.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/puzzle-piece-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/view/repo_tag_simple_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | -------------------------------------------------------------------------------- /src/view/top_page_action_bar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 15 | top-page.kill 16 | Kill 17 | center 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/stacked-plates-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/cross-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/whole-word-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v8 14 | with: 15 | only-labels: "needs info" 16 | days-before-issue-stale: 30 17 | days-before-issue-close: 14 18 | stale-issue-label: "stale" 19 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 20 | close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." 21 | days-before-pr-stale: -1 22 | days-before-pr-close: -1 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/bell-outline-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/view/containers_list_view.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/city-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/eraser5-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /data/resources/style-hc.css: -------------------------------------------------------------------------------- 1 | containercard.not-running { 2 | background-color: var(--sidebar-bg-color); 3 | } 4 | 5 | .dim-icon { 6 | color: #ffffff; 7 | } 8 | 9 | .tag-label { 10 | font-weight: bold; 11 | } 12 | 13 | .container-health-status-healthy, 14 | .container-status-running, 15 | .image-used, 16 | .pod-status-running, 17 | .volume-used { 18 | background-color: var(--accent-bg-color); 19 | color: var(--accent-fg-color); 20 | } 21 | 22 | .pod-status-degraded { 23 | background-color: var(--accent-yellow); 24 | color: var(--accent-fg-color); 25 | } 26 | 27 | .container-status-dead, 28 | .container-status-unknown, 29 | .pod-status-error, 30 | .pod-status-dead, 31 | .pod-status-unknown, 32 | .container-health-status-unhealthy, 33 | .container-health-status-unknown { 34 | background-color: var(--error-bg-color); 35 | color: var(--error-fg-color); 36 | } 37 | 38 | .container-health-status-healthy { 39 | color: var(--accent-fg-color); 40 | background-color: var(--accent-green); 41 | } 42 | -------------------------------------------------------------------------------- /src/view/volumes_group.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | volumes-group.create-volume 5 | False 6 | 7 | 8 | 9 | list-add-symbolic 10 | 15 11 | 15 12 | Create Volume 13 | 14 | 15 | 16 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/build-configure-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/view/containers_group.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | containers-group.create-container 5 | False 6 | 7 | 8 | 9 | list-add-symbolic 10 | 15 11 | 15 12 | Create Container 13 | 14 | 15 | 16 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | mod circular_progress_bar; 2 | mod count_badge; 3 | mod date_time_row; 4 | mod main_menu_button; 5 | mod property_row; 6 | mod property_widget_row; 7 | mod random_name_entry_row; 8 | mod scalable_text_view; 9 | mod source_view_search_widget; 10 | mod spinner; 11 | mod text_search_entry; 12 | mod zoom_control; 13 | 14 | pub(crate) use self::circular_progress_bar::CircularProgressBar; 15 | pub(crate) use self::count_badge::CountBadge; 16 | pub(crate) use self::date_time_row::DateTimeRow; 17 | pub(crate) use self::main_menu_button::MainMenuButton; 18 | pub(crate) use self::property_row::PropertyRow; 19 | pub(crate) use self::property_widget_row::PropertyWidgetRow; 20 | pub(crate) use self::random_name_entry_row::RandomNameEntryRow; 21 | pub(crate) use self::scalable_text_view::ScalableTextView; 22 | pub(crate) use self::source_view_search_widget::SourceViewSearchWidget; 23 | pub(crate) use self::spinner::Spinner; 24 | pub(crate) use self::text_search_entry::TextSearchEntry; 25 | pub(crate) use self::zoom_control::ZoomControl; 26 | -------------------------------------------------------------------------------- /src/view/actions_button.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | 36 | -------------------------------------------------------------------------------- /src/view/image_menu_button.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | _Create Container… 7 | image-menu-button.create-container 8 | action-disabled 9 | 10 | 11 | Delete 12 | image-menu-button.delete-image 13 | action-disabled 14 | 15 | 16 | 17 | 30 | 31 | -------------------------------------------------------------------------------- /src/widget/main_menu_button.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | _Keyboard Shortcuts 8 | app.shortcuts 9 | 10 | 11 | _About Pods 12 | app.about 13 | 14 |
15 |
16 | 17 | 32 |
33 | -------------------------------------------------------------------------------- /Pods.doap: -------------------------------------------------------------------------------- 1 | 6 | 7 | Pods 8 | Manage your Podman containers 9 | 10 | 11 | Rust 12 | 13 | 14 | 15 | Marcus Behrendt 16 | 17 | 18 | 19 | 20 | marhkb 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/widget/property_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/ambulance-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/view/containers_grid_view.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/view/image_pull_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | -------------------------------------------------------------------------------- /src/view/image_selection_combo_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 33 | 34 | -------------------------------------------------------------------------------- /src/view/connection.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use gettextrs::gettext; 3 | use gtk::glib; 4 | 5 | use crate::model; 6 | use crate::utils; 7 | 8 | pub(crate) fn show_ongoing_actions_warning_dialog>( 9 | widget: &W, 10 | connection_manager: &model::ConnectionManager, 11 | heading: &str, 12 | ) -> bool { 13 | if connection_manager 14 | .client() 15 | .map(|client| client.action_list().ongoing() > 0) 16 | .unwrap_or(false) 17 | { 18 | let dialog = adw::MessageDialog::builder() 19 | .heading(heading) 20 | .body_use_markup(true) 21 | .body(gettext( 22 | "There are ongoing actions whose progress will be irretrievably lost", 23 | )) 24 | .transient_for(&utils::root(widget)) 25 | .build(); 26 | 27 | dialog.add_responses(&[ 28 | ("cancel", &gettext("_Cancel")), 29 | ("confirm", &gettext("_Confirm")), 30 | ]); 31 | dialog.set_default_response(Some("cancel")); 32 | dialog.set_response_appearance("confirm", adw::ResponseAppearance::Destructive); 33 | 34 | glib::MainContext::default().block_on(dialog.choose_future()) == "confirm" 35 | } else { 36 | true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/widget/main_menu_button.rs: -------------------------------------------------------------------------------- 1 | use adw::subclass::prelude::*; 2 | use gtk::CompositeTemplate; 3 | use gtk::glib; 4 | 5 | use crate::utils; 6 | 7 | mod imp { 8 | use super::*; 9 | 10 | #[derive(Debug, Default, CompositeTemplate)] 11 | #[template(resource = "/com/github/marhkb/Pods/ui/widget/main_menu_button.ui")] 12 | pub(crate) struct MainMenuButton; 13 | 14 | #[glib::object_subclass] 15 | impl ObjectSubclass for MainMenuButton { 16 | const NAME: &'static str = "PdsMainMenuButton"; 17 | type Type = super::MainMenuButton; 18 | type ParentType = gtk::Widget; 19 | 20 | fn class_init(klass: &mut Self::Class) { 21 | klass.bind_template(); 22 | } 23 | 24 | fn instance_init(obj: &glib::subclass::InitializingObject) { 25 | obj.init_template(); 26 | } 27 | } 28 | 29 | impl ObjectImpl for MainMenuButton { 30 | fn dispose(&self) { 31 | utils::unparent_children(&*self.obj()); 32 | } 33 | } 34 | 35 | impl WidgetImpl for MainMenuButton {} 36 | } 37 | 38 | glib::wrapper! { 39 | pub(crate) struct MainMenuButton(ObjectSubclass) 40 | @extends gtk::Widget, 41 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 42 | } 43 | -------------------------------------------------------------------------------- /src/widget/zoom_control.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46 | 47 | -------------------------------------------------------------------------------- /src/widget/source_view_search_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 39 | 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pods" 3 | version = "2.2.0" 4 | authors = ["Marcus Behrendt "] 5 | edition = "2024" 6 | 7 | [dependencies] 8 | astral-tokio-tar = { version = "0.5.6", default-features = false } 9 | adw = { version = "0.8", package = "libadwaita", features = ["v1_8"] } 10 | anyhow = "1" 11 | ashpd = { version = "0.12", default-features = false, features = ["gtk4", "tokio"] } 12 | futures = { version = "0.3", default-features = false } 13 | gettext-rs = { version = "=0.7.0", features = ["gettext-system"] } 14 | gtk = { version = "0.10", package = "gtk4", features = ["gnome_48"] } 15 | indexmap = { version = "2", features = ["serde"] } 16 | log = "0.4" 17 | multi_log = "0.1" 18 | names = { version = "0.14", default-features = false } 19 | oo7 = { version = "0.5", default-features = false, features = ["native_crypto", "tokio"] } 20 | paste = "1" 21 | podman-api = { git = "https://github.com/vv9k/podman-api-rs.git", rev = "363d945b9b9905c50dfa0bfe0f9331f9fdeef079", default-features = false } 22 | serde = "1" 23 | serde_json = "1" 24 | simplelog = { version = "0.12", features = ["paris"] } 25 | sourceview5 = { version = "0.10" } 26 | syslog = "7" 27 | tokio = "1" 28 | tokio-stream = { version = "0.1", default-features = false } 29 | vte = { version = "0.15", default-features = false } 30 | vte4 = "0.9" 31 | 32 | [profile.release] 33 | lto = true 34 | codegen-units = 1 35 | -------------------------------------------------------------------------------- /src/widget/count_badge.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45 | 46 | -------------------------------------------------------------------------------- /src/model/pod_data.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | 3 | use glib::Properties; 4 | use glib::prelude::*; 5 | use glib::subclass::prelude::*; 6 | use gtk::glib; 7 | 8 | use crate::podman; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties)] 14 | #[properties(wrapper_type = super::PodData)] 15 | pub(crate) struct PodData { 16 | #[property(get, set, construct_only)] 17 | pub(super) hostname: OnceCell, 18 | } 19 | 20 | #[glib::object_subclass] 21 | impl ObjectSubclass for PodData { 22 | const NAME: &'static str = "PodData"; 23 | type Type = super::PodData; 24 | } 25 | 26 | impl ObjectImpl for PodData { 27 | fn properties() -> &'static [glib::ParamSpec] { 28 | Self::derived_properties() 29 | } 30 | 31 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 32 | self.derived_set_property(id, value, pspec); 33 | } 34 | 35 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 36 | self.derived_property(id, pspec) 37 | } 38 | } 39 | } 40 | 41 | glib::wrapper! { 42 | pub(crate) struct PodData(ObjectSubclass); 43 | } 44 | 45 | impl From<&podman::models::InspectPodData> for PodData { 46 | fn from(data: &podman::models::InspectPodData) -> Self { 47 | glib::Object::builder() 48 | .property("hostname", data.hostname.as_deref().unwrap_or_default()) 49 | .build() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/status/local-connection-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 30 | 32 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/view/container_health_check_log_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 49 | 50 | -------------------------------------------------------------------------------- /src/view/key_val_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46 | 47 | -------------------------------------------------------------------------------- /src/model/selectable.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use glib::prelude::*; 4 | use glib::subclass::prelude::*; 5 | use gtk::glib; 6 | 7 | mod imp { 8 | use super::*; 9 | 10 | #[allow(dead_code)] 11 | #[derive(Copy, Clone, Debug)] 12 | pub(crate) struct SelectableClass(glib::gobject_ffi::GTypeInterface); 13 | 14 | unsafe impl InterfaceStruct for SelectableClass { 15 | type Type = Selectable; 16 | } 17 | 18 | pub(crate) struct Selectable; 19 | 20 | #[glib::object_interface] 21 | impl ObjectInterface for Selectable { 22 | const NAME: &'static str = "Selectable"; 23 | type Interface = SelectableClass; 24 | 25 | fn properties() -> &'static [glib::ParamSpec] { 26 | static PROPERTIES: OnceLock> = OnceLock::new(); 27 | PROPERTIES.get_or_init(|| { 28 | vec![ 29 | glib::ParamSpecBoolean::builder("selected") 30 | .explicit_notify() 31 | .build(), 32 | ] 33 | }) 34 | } 35 | } 36 | } 37 | 38 | glib::wrapper! { pub(crate) struct Selectable(ObjectInterface); } 39 | 40 | pub(crate) trait SelectableExt: IsA { 41 | fn is_selected(&self) -> bool; 42 | 43 | fn set_selected(&self, value: bool); 44 | 45 | fn select(&self) { 46 | self.set_selected(!self.is_selected()); 47 | } 48 | } 49 | 50 | impl> SelectableExt for T { 51 | fn is_selected(&self) -> bool { 52 | self.property("selected") 53 | } 54 | 55 | fn set_selected(&self, value: bool) { 56 | self.set_property("selected", value); 57 | } 58 | } 59 | 60 | unsafe impl IsImplementable for Selectable {} 61 | -------------------------------------------------------------------------------- /src/view/info_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63 | 64 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/actions/pip-out-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 30 | 34 | 38 | 43 | 44 | -------------------------------------------------------------------------------- /src/view/pods_row.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::utils; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties, CompositeTemplate)] 14 | #[properties(wrapper_type = super::PodsRow)] 15 | #[template(resource = "/com/github/marhkb/Pods/ui/view/pods_row.ui")] 16 | pub(crate) struct PodsRow { 17 | #[property(get, set)] 18 | pub(super) pod_list: glib::WeakRef, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for PodsRow { 23 | const NAME: &'static str = "PdsPodsRow"; 24 | type Type = super::PodsRow; 25 | type ParentType = gtk::Widget; 26 | 27 | fn class_init(klass: &mut Self::Class) { 28 | klass.bind_template(); 29 | } 30 | 31 | fn instance_init(obj: &glib::subclass::InitializingObject) { 32 | obj.init_template(); 33 | } 34 | } 35 | 36 | impl ObjectImpl for PodsRow { 37 | fn properties() -> &'static [glib::ParamSpec] { 38 | Self::derived_properties() 39 | } 40 | 41 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 42 | self.derived_set_property(id, value, pspec); 43 | } 44 | 45 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 46 | self.derived_property(id, pspec) 47 | } 48 | 49 | fn dispose(&self) { 50 | utils::unparent_children(&*self.obj()); 51 | } 52 | } 53 | 54 | impl WidgetImpl for PodsRow {} 55 | } 56 | 57 | glib::wrapper! { 58 | pub(crate) struct PodsRow(ObjectSubclass) 59 | @extends gtk::Widget, 60 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 61 | } 62 | -------------------------------------------------------------------------------- /src/view/images_row.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::utils; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties, CompositeTemplate)] 14 | #[properties(wrapper_type = super::ImagesRow)] 15 | #[template(resource = "/com/github/marhkb/Pods/ui/view/images_row.ui")] 16 | pub(crate) struct ImagesRow { 17 | #[property(get, set)] 18 | pub(super) image_list: glib::WeakRef, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for ImagesRow { 23 | const NAME: &'static str = "PdsImagesRow"; 24 | type Type = super::ImagesRow; 25 | type ParentType = gtk::Widget; 26 | 27 | fn class_init(klass: &mut Self::Class) { 28 | klass.bind_template(); 29 | } 30 | 31 | fn instance_init(obj: &glib::subclass::InitializingObject) { 32 | obj.init_template(); 33 | } 34 | } 35 | 36 | impl ObjectImpl for ImagesRow { 37 | fn properties() -> &'static [glib::ParamSpec] { 38 | Self::derived_properties() 39 | } 40 | 41 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 42 | self.derived_set_property(id, value, pspec); 43 | } 44 | 45 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 46 | self.derived_property(id, pspec) 47 | } 48 | 49 | fn dispose(&self) { 50 | utils::unparent_children(&*self.obj()); 51 | } 52 | } 53 | 54 | impl WidgetImpl for ImagesRow {} 55 | } 56 | 57 | glib::wrapper! { 58 | pub(crate) struct ImagesRow(ObjectSubclass) 59 | @extends gtk::Widget, 60 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 61 | } 62 | -------------------------------------------------------------------------------- /src/view/image_history_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61 | 62 | -------------------------------------------------------------------------------- /src/view/volumes_row.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::utils; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties, CompositeTemplate)] 14 | #[properties(wrapper_type = super::VolumesRow)] 15 | #[template(resource = "/com/github/marhkb/Pods/ui/view/volumes_row.ui")] 16 | pub(crate) struct VolumesRow { 17 | #[property(get, set)] 18 | pub(super) volume_list: glib::WeakRef, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for VolumesRow { 23 | const NAME: &'static str = "PdsVolumesRow"; 24 | type Type = super::VolumesRow; 25 | type ParentType = gtk::Widget; 26 | 27 | fn class_init(klass: &mut Self::Class) { 28 | klass.bind_template(); 29 | } 30 | 31 | fn instance_init(obj: &glib::subclass::InitializingObject) { 32 | obj.init_template(); 33 | } 34 | } 35 | 36 | impl ObjectImpl for VolumesRow { 37 | fn properties() -> &'static [glib::ParamSpec] { 38 | Self::derived_properties() 39 | } 40 | 41 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 42 | self.derived_set_property(id, value, pspec); 43 | } 44 | 45 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 46 | self.derived_property(id, pspec) 47 | } 48 | 49 | fn dispose(&self) { 50 | utils::unparent_children(&*self.obj()); 51 | } 52 | } 53 | 54 | impl WidgetImpl for VolumesRow {} 55 | } 56 | 57 | glib::wrapper! { 58 | pub(crate) struct VolumesRow(ObjectSubclass) 59 | @extends gtk::Widget, 60 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 61 | } 62 | -------------------------------------------------------------------------------- /src/view/welcome_page.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::utils; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties, CompositeTemplate)] 14 | #[properties(wrapper_type = super::WelcomePage)] 15 | #[template(resource = "/com/github/marhkb/Pods/ui/view/welcome_page.ui")] 16 | pub(crate) struct WelcomePage { 17 | #[property(get, set, nullable)] 18 | pub(super) connection_manager: glib::WeakRef, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for WelcomePage { 23 | const NAME: &'static str = "PdsWelcomePage"; 24 | type Type = super::WelcomePage; 25 | type ParentType = gtk::Widget; 26 | 27 | fn class_init(klass: &mut Self::Class) { 28 | klass.bind_template(); 29 | } 30 | 31 | fn instance_init(obj: &glib::subclass::InitializingObject) { 32 | obj.init_template(); 33 | } 34 | } 35 | 36 | impl ObjectImpl for WelcomePage { 37 | fn properties() -> &'static [glib::ParamSpec] { 38 | Self::derived_properties() 39 | } 40 | 41 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 42 | self.derived_set_property(id, value, pspec); 43 | } 44 | 45 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 46 | self.derived_property(id, pspec) 47 | } 48 | 49 | fn dispose(&self) { 50 | utils::unparent_children(&*self.obj()); 51 | } 52 | } 53 | 54 | impl WidgetImpl for WelcomePage {} 55 | } 56 | 57 | glib::wrapper! { 58 | pub(crate) struct WelcomePage(ObjectSubclass) 59 | @extends gtk::Widget, 60 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 61 | } 62 | -------------------------------------------------------------------------------- /src/view/containers_row.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::utils; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties, CompositeTemplate)] 14 | #[properties(wrapper_type = super::ContainersRow)] 15 | #[template(resource = "/com/github/marhkb/Pods/ui/view/containers_row.ui")] 16 | pub(crate) struct ContainersRow { 17 | #[property(get, set)] 18 | pub(super) container_list: glib::WeakRef, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for ContainersRow { 23 | const NAME: &'static str = "PdsContainersRow"; 24 | type Type = super::ContainersRow; 25 | type ParentType = gtk::Widget; 26 | 27 | fn class_init(klass: &mut Self::Class) { 28 | klass.bind_template(); 29 | } 30 | 31 | fn instance_init(obj: &glib::subclass::InitializingObject) { 32 | obj.init_template(); 33 | } 34 | } 35 | 36 | impl ObjectImpl for ContainersRow { 37 | fn properties() -> &'static [glib::ParamSpec] { 38 | Self::derived_properties() 39 | } 40 | 41 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 42 | self.derived_set_property(id, value, pspec); 43 | } 44 | 45 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 46 | self.derived_property(id, pspec) 47 | } 48 | 49 | fn dispose(&self) { 50 | utils::unparent_children(&*self.obj()); 51 | } 52 | } 53 | 54 | impl WidgetImpl for ContainersRow {} 55 | } 56 | 57 | glib::wrapper! { 58 | pub(crate) struct ContainersRow(ObjectSubclass) 59 | @extends gtk::Widget, 60 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 61 | } 62 | -------------------------------------------------------------------------------- /src/view/top_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 59 | 60 | -------------------------------------------------------------------------------- /src/model/container_volume_list.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use gio::prelude::*; 4 | use gio::subclass::prelude::*; 5 | use gtk::gio; 6 | use gtk::glib; 7 | use indexmap::map::IndexMap; 8 | 9 | use crate::model; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default)] 15 | pub(crate) struct ContainerVolumeList( 16 | pub(super) RefCell>, 17 | ); 18 | 19 | #[glib::object_subclass] 20 | impl ObjectSubclass for ContainerVolumeList { 21 | const NAME: &'static str = "ContainerVolumeList"; 22 | type Type = super::ContainerVolumeList; 23 | type Interfaces = (gio::ListModel,); 24 | } 25 | 26 | impl ObjectImpl for ContainerVolumeList {} 27 | 28 | impl ListModelImpl for ContainerVolumeList { 29 | fn item_type(&self) -> glib::Type { 30 | model::ContainerVolume::static_type() 31 | } 32 | 33 | fn n_items(&self) -> u32 { 34 | self.0.borrow().len() as u32 35 | } 36 | 37 | fn item(&self, position: u32) -> Option { 38 | self.0 39 | .borrow() 40 | .get_index(position as usize) 41 | .map(|(_, obj)| obj.clone().upcast()) 42 | } 43 | } 44 | } 45 | 46 | glib::wrapper! { 47 | pub(crate) struct ContainerVolumeList(ObjectSubclass) 48 | @implements gio::ListModel; 49 | } 50 | 51 | impl Default for ContainerVolumeList { 52 | fn default() -> Self { 53 | glib::Object::builder().build() 54 | } 55 | } 56 | 57 | impl ContainerVolumeList { 58 | pub(crate) fn add_volume(&self, container_volume: model::ContainerVolume) { 59 | if let Some(ref volume) = container_volume.volume() { 60 | let (index, _) = self 61 | .imp() 62 | .0 63 | .borrow_mut() 64 | .insert_full(volume.inner().name.clone(), container_volume); 65 | 66 | self.items_changed(index as u32, 0, 1); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/view/action_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61 | 62 | -------------------------------------------------------------------------------- /src/model/value.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::sync::OnceLock; 3 | 4 | use glib::Properties; 5 | // use gtk::glib::subclass::Signal; 6 | use glib::prelude::*; 7 | use glib::subclass::Signal; 8 | use glib::subclass::prelude::*; 9 | use gtk::glib; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties)] 15 | #[properties(wrapper_type = super::Value)] 16 | pub(crate) struct Value { 17 | #[property(name = "value", get, set)] 18 | pub(super) inner: RefCell, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for Value { 23 | const NAME: &'static str = "Value"; 24 | type Type = super::Value; 25 | } 26 | 27 | impl ObjectImpl for Value { 28 | fn signals() -> &'static [Signal] { 29 | static SIGNALS: OnceLock> = OnceLock::new(); 30 | SIGNALS.get_or_init(|| vec![Signal::builder("remove-request").build()]) 31 | } 32 | 33 | fn properties() -> &'static [glib::ParamSpec] { 34 | Self::derived_properties() 35 | } 36 | 37 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 38 | self.derived_set_property(id, value, pspec); 39 | } 40 | 41 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 42 | self.derived_property(id, pspec) 43 | } 44 | } 45 | } 46 | 47 | glib::wrapper! { 48 | pub(crate) struct Value(ObjectSubclass); 49 | } 50 | 51 | impl Default for Value { 52 | fn default() -> Self { 53 | glib::Object::builder().build() 54 | } 55 | } 56 | 57 | impl Value { 58 | pub(crate) fn remove_request(&self) { 59 | self.emit_by_name::<()>("remove-request", &[]); 60 | } 61 | 62 | pub(crate) fn connect_remove_request( 63 | &self, 64 | f: F, 65 | ) -> glib::SignalHandlerId { 66 | self.connect_local("remove-request", true, move |values| { 67 | let obj = values[0].get::().unwrap(); 68 | f(&obj); 69 | 70 | None 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/model/health_check_log.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | 3 | use glib::Properties; 4 | use glib::prelude::*; 5 | use glib::subclass::prelude::*; 6 | use gtk::gio; 7 | use gtk::glib; 8 | 9 | use crate::model; 10 | use crate::podman; 11 | 12 | mod imp { 13 | use super::*; 14 | 15 | #[derive(Debug, Default, Properties)] 16 | #[properties(wrapper_type = super::HealthCheckLog)] 17 | pub(crate) struct HealthCheckLog { 18 | #[property(get, set, construct_only)] 19 | pub(super) end: OnceCell, 20 | #[property(get, set, construct_only)] 21 | pub(super) exit_code: OnceCell, 22 | #[property(get, set, construct_only)] 23 | pub(super) output: OnceCell, 24 | #[property(get, set, construct_only)] 25 | pub(super) start: OnceCell, 26 | } 27 | 28 | #[glib::object_subclass] 29 | impl ObjectSubclass for HealthCheckLog { 30 | const NAME: &'static str = "HealthCheckLog"; 31 | type Type = super::HealthCheckLog; 32 | } 33 | 34 | impl ObjectImpl for HealthCheckLog { 35 | fn properties() -> &'static [glib::ParamSpec] { 36 | Self::derived_properties() 37 | } 38 | 39 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 40 | self.derived_set_property(id, value, pspec); 41 | } 42 | 43 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 44 | self.derived_property(id, pspec) 45 | } 46 | } 47 | } 48 | 49 | glib::wrapper! { 50 | pub(crate) struct HealthCheckLog(ObjectSubclass) 51 | @implements gio::ListModel, model::SelectableList; 52 | } 53 | 54 | impl From<&podman::models::HealthCheckLog> for HealthCheckLog { 55 | fn from(data: &podman::models::HealthCheckLog) -> Self { 56 | glib::Object::builder() 57 | .property("end", data.end.as_ref().unwrap()) 58 | .property("exit-code", data.exit_code.unwrap()) 59 | .property("output", data.output.as_ref().unwrap()) 60 | .property("start", data.start.as_ref().unwrap()) 61 | .build() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /hooks/pre-commit.hook: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook 3 | 4 | install_rustfmt() { 5 | if ! which rustup &> /dev/null; then 6 | curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain none -y 7 | export PATH=$PATH:$HOME/.cargo/bin 8 | if ! which rustup &> /dev/null; then 9 | echo "Failed to install rustup. Performing the commit without style checking." 10 | exit 0 11 | fi 12 | fi 13 | 14 | if ! rustup toolchain list | grep nightly &> /dev/null; then 15 | echo "Installing minimal rust nightly toolchain…" 16 | rustup toolchain install nightly --profile minimal 17 | fi 18 | 19 | if ! rustup component list --toolchain=nightly | grep rustfmt &> /dev/null; then 20 | echo "Installing rustfmt…" 21 | rustup component add rustfmt --toolchain=nightly 22 | fi 23 | } 24 | 25 | if ! which cargo >/dev/null 2>&1 || ! cargo +nightly fmt --help >/dev/null 2>&1; then 26 | echo "Unable to check the project’s code style, because rustfmt could not be run." 27 | 28 | if [ ! -t 1 ]; then 29 | # No input is possible 30 | echo "Performing commit." 31 | exit 0 32 | fi 33 | 34 | echo "" 35 | echo "y: Install rustfmt via rustup" 36 | echo "n: Don't install rustfmt and perform the commit" 37 | echo "Q: Don't install rustfmt and abort the commit" 38 | 39 | echo "" 40 | while true 41 | do 42 | echo -n "Install rustfmt via rustup? [y/n/Q]: "; read yn < /dev/tty 43 | case $yn in 44 | [Yy]* ) install_rustfmt; break;; 45 | [Nn]* ) echo "Performing commit."; exit 0;; 46 | [Qq]* | "" ) echo "Aborting commit."; exit -1 >/dev/null 2>&1;; 47 | * ) echo "Invalid input";; 48 | esac 49 | done 50 | 51 | fi 52 | 53 | echo "--Checking style--" 54 | cargo +nightly fmt --all -- --check 55 | if test $? != 0; then 56 | echo "--Checking style fail--" 57 | echo "Please fix the above issues, either manually or by running: cargo +nightly fmt --all" 58 | 59 | exit -1 60 | else 61 | echo "--Checking style pass--" 62 | fi 63 | -------------------------------------------------------------------------------- /src/model/key_val.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::sync::OnceLock; 3 | 4 | use glib::Properties; 5 | use glib::prelude::*; 6 | use glib::subclass::Signal; 7 | use glib::subclass::prelude::*; 8 | use gtk::glib; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties)] 14 | #[properties(wrapper_type = super::KeyVal)] 15 | pub(crate) struct KeyVal { 16 | #[property(get, set)] 17 | pub(super) key: RefCell, 18 | #[property(get, set)] 19 | pub(super) value: RefCell, 20 | } 21 | 22 | #[glib::object_subclass] 23 | impl ObjectSubclass for KeyVal { 24 | const NAME: &'static str = "KeyVal"; 25 | type Type = super::KeyVal; 26 | } 27 | 28 | impl ObjectImpl for KeyVal { 29 | fn signals() -> &'static [Signal] { 30 | static SIGNALS: OnceLock> = OnceLock::new(); 31 | SIGNALS.get_or_init(|| vec![Signal::builder("remove-request").build()]) 32 | } 33 | 34 | fn properties() -> &'static [glib::ParamSpec] { 35 | Self::derived_properties() 36 | } 37 | 38 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 39 | self.derived_set_property(id, value, pspec); 40 | } 41 | 42 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 43 | self.derived_property(id, pspec) 44 | } 45 | } 46 | } 47 | 48 | glib::wrapper! { 49 | pub(crate) struct KeyVal(ObjectSubclass); 50 | } 51 | 52 | impl Default for KeyVal { 53 | fn default() -> Self { 54 | glib::Object::builder().build() 55 | } 56 | } 57 | 58 | impl KeyVal { 59 | pub(crate) fn remove_request(&self) { 60 | self.emit_by_name::<()>("remove-request", &[]); 61 | } 62 | 63 | pub(crate) fn connect_remove_request( 64 | &self, 65 | f: F, 66 | ) -> glib::SignalHandlerId { 67 | self.connect_local("remove-request", true, move |values| { 68 | let obj = values[0].get::().unwrap(); 69 | f(&obj); 70 | 71 | None 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/model/image_data.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | 3 | use glib::Properties; 4 | use glib::prelude::*; 5 | use glib::subclass::prelude::*; 6 | use gtk::glib; 7 | 8 | use crate::model; 9 | use crate::podman; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties)] 15 | #[properties(wrapper_type = super::ImageData)] 16 | pub(crate) struct ImageData { 17 | #[property(get, set, construct_only, nullable)] 18 | pub(super) architecture: OnceCell>, 19 | #[property(get, set, construct_only, nullable)] 20 | pub(super) author: OnceCell>, 21 | #[property(get, set, construct_only, nullable)] 22 | pub(super) comment: OnceCell>, 23 | #[property(get, set, construct_only)] 24 | pub(super) config: OnceCell, 25 | } 26 | 27 | #[glib::object_subclass] 28 | impl ObjectSubclass for ImageData { 29 | const NAME: &'static str = "ImageData"; 30 | type Type = super::ImageData; 31 | } 32 | 33 | impl ObjectImpl for ImageData { 34 | fn properties() -> &'static [glib::ParamSpec] { 35 | Self::derived_properties() 36 | } 37 | 38 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 39 | self.derived_set_property(id, value, pspec); 40 | } 41 | 42 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 43 | self.derived_property(id, pspec) 44 | } 45 | } 46 | } 47 | 48 | glib::wrapper! { 49 | pub(crate) struct ImageData(ObjectSubclass); 50 | } 51 | 52 | impl From<&podman::models::ImageData> for ImageData { 53 | fn from(data: &podman::models::ImageData) -> Self { 54 | glib::Object::builder() 55 | .property("architecture", data.architecture.as_ref()) 56 | .property("author", data.author.as_ref()) 57 | .property("comment", data.comment.as_ref()) 58 | .property( 59 | "config", 60 | model::ImageConfig::from_libpod(data.config.as_ref().unwrap()), 61 | ) 62 | .build() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/model/container_volume.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | 3 | use glib::Properties; 4 | use glib::prelude::*; 5 | use glib::subclass::prelude::*; 6 | use gtk::glib; 7 | 8 | use crate::model; 9 | use crate::monad_boxed_type; 10 | use crate::podman; 11 | 12 | monad_boxed_type!(pub(crate) BoxedInspectMount(podman::models::InspectMount) impls Debug); 13 | 14 | mod imp { 15 | use super::*; 16 | 17 | #[derive(Debug, Default, Properties)] 18 | #[properties(wrapper_type = super::ContainerVolume)] 19 | pub(crate) struct ContainerVolume { 20 | #[property(get, set, construct_only)] 21 | pub(super) container_volume_list: glib::WeakRef, 22 | #[property(get, set, construct_only)] 23 | pub(super) volume: glib::WeakRef, 24 | #[property(get, set, construct_only)] 25 | pub(super) inner: OnceCell, 26 | } 27 | 28 | #[glib::object_subclass] 29 | impl ObjectSubclass for ContainerVolume { 30 | const NAME: &'static str = "ContainerVolume"; 31 | type Type = super::ContainerVolume; 32 | } 33 | 34 | impl ObjectImpl for ContainerVolume { 35 | fn properties() -> &'static [glib::ParamSpec] { 36 | Self::derived_properties() 37 | } 38 | 39 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 40 | self.derived_set_property(id, value, pspec); 41 | } 42 | 43 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 44 | self.derived_property(id, pspec) 45 | } 46 | } 47 | } 48 | 49 | glib::wrapper! { 50 | pub(crate) struct ContainerVolume(ObjectSubclass); 51 | } 52 | 53 | impl ContainerVolume { 54 | pub(crate) fn new( 55 | container_volume_list: &model::ContainerVolumeList, 56 | volume: &model::Volume, 57 | inner: podman::models::InspectMount, 58 | ) -> Self { 59 | glib::Object::builder() 60 | .property("container-volume-list", container_volume_list) 61 | .property("volume", volume) 62 | .property("inner", BoxedInspectMount::from(inner)) 63 | .build() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/model/repo_tag.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::cell::OnceCell; 3 | 4 | use glib::Properties; 5 | use glib::prelude::*; 6 | use glib::subclass::prelude::*; 7 | use gtk::glib; 8 | 9 | use crate::model; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties)] 15 | #[properties(wrapper_type = super::RepoTag)] 16 | pub(crate) struct RepoTag { 17 | #[property(get, set, construct_only, nullable)] 18 | pub(super) repo_tag_list: glib::WeakRef, 19 | #[property(get, set, construct_only)] 20 | pub(super) full: OnceCell, 21 | #[property(get, set)] 22 | pub(super) to_be_deleted: Cell, 23 | } 24 | 25 | #[glib::object_subclass] 26 | impl ObjectSubclass for RepoTag { 27 | const NAME: &'static str = "RepoTag"; 28 | type Type = super::RepoTag; 29 | } 30 | 31 | impl ObjectImpl for RepoTag { 32 | fn properties() -> &'static [glib::ParamSpec] { 33 | Self::derived_properties() 34 | } 35 | 36 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 37 | self.derived_set_property(id, value, pspec); 38 | } 39 | 40 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 41 | self.derived_property(id, pspec) 42 | } 43 | } 44 | } 45 | 46 | glib::wrapper! { 47 | pub(crate) struct RepoTag(ObjectSubclass); 48 | } 49 | 50 | impl RepoTag { 51 | pub(crate) fn new(repo_tag_list: &model::RepoTagList, full: &str) -> Self { 52 | glib::Object::builder() 53 | .property("repo-tag-list", repo_tag_list) 54 | .property("full", full) 55 | .build() 56 | } 57 | 58 | pub(crate) fn host(&self) -> String { 59 | self.full().split_once('/').unwrap().0.to_owned() 60 | } 61 | 62 | pub(crate) fn namespace(&self) -> String { 63 | self.full().split_once('/').unwrap().1.to_owned() 64 | } 65 | 66 | pub(crate) fn repo(&self) -> String { 67 | self.full().split_once(':').unwrap().0.to_owned() 68 | } 69 | 70 | pub(crate) fn tag(&self) -> String { 71 | self.full().split_once(':').unwrap().1.to_owned() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/view/container_volume_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64 | 65 | -------------------------------------------------------------------------------- /src/view/repo_tag_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | -------------------------------------------------------------------------------- /src/view/pod_menu_button.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | _Create Container… 8 | pod-menu-button.create-container 9 | 10 |
11 |
12 | 13 | _Start 14 | pod-menu-button.start 15 | action-disabled 16 | 17 | 18 | S_top 19 | pod-menu-button.stop 20 | action-disabled 21 | 22 | 23 | R_estart 24 | pod-menu-button.restart 25 | action-disabled 26 | 27 | 28 | _Resume 29 | pod-menu-button.resume 30 | action-disabled 31 | 32 | 33 | _Pause 34 | pod-menu-button.pause 35 | action-disabled 36 | 37 |
38 |
39 | 40 | _Delete 41 | pod-menu-button.delete 42 | action-disabled 43 | 44 |
45 |
46 | 47 | 60 |
61 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'pods', 3 | 'rust', 4 | version: '2.2.0', 5 | meson_version: '>= 0.59', 6 | license: 'GPL-3.0', 7 | ) 8 | 9 | i18n = import('i18n') 10 | gnome = import('gnome') 11 | 12 | base_id = 'com.github.marhkb.Pods' 13 | 14 | dependency('glib-2.0', version: '>= 2.66') 15 | dependency('gio-2.0', version: '>= 2.66') 16 | dependency('gtk4', version: '>= 4.18.0') 17 | dependency('libadwaita-1', version: '>= 1.7') 18 | dependency('gtksourceview-5', version: '>= 4.90') 19 | dependency('vte-2.91-gtk4', version: '>= 0.70.0') 20 | 21 | glib_compile_resources = find_program('glib-compile-resources', required: true) 22 | glib_compile_schemas = find_program('glib-compile-schemas', required: true) 23 | desktop_file_validate = find_program('desktop-file-validate', required: false) 24 | appstream_util = find_program('appstream-util', required: false) 25 | cargo = find_program('cargo', required: true) 26 | 27 | version = meson.project_version() 28 | 29 | prefix = get_option('prefix') 30 | bindir = prefix / get_option('bindir') 31 | localedir = prefix / get_option('localedir') 32 | 33 | datadir = prefix / get_option('datadir') 34 | pkgdatadir = datadir / meson.project_name() 35 | iconsdir = datadir / 'icons' 36 | podir = meson.project_source_root() / 'po' 37 | gettext_package = meson.project_name() 38 | 39 | if get_option('profile') == 'development' 40 | profile = 'Devel' 41 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() 42 | if vcs_tag == '' 43 | version_suffix = '-devel' 44 | else 45 | version_suffix = '-@0@'.format(vcs_tag) 46 | endif 47 | application_id = '@0@.@1@'.format(base_id, profile) 48 | else 49 | profile = '' 50 | version_suffix = '' 51 | application_id = base_id 52 | endif 53 | 54 | meson.add_dist_script( 55 | 'build-aux/dist-vendor.sh', 56 | meson.project_build_root() / 'meson-dist' / meson.project_name() + '-' + version, 57 | meson.project_source_root() 58 | ) 59 | 60 | if get_option('profile') == 'development' 61 | # Setup pre-commit hook for ensuring coding style is always consistent 62 | message('Setting up git pre-commit hook..') 63 | run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit') 64 | endif 65 | 66 | subdir('data') 67 | subdir('po') 68 | subdir('src') 69 | 70 | gnome.post_install( 71 | gtk_update_icon_cache: true, 72 | glib_compile_schemas: true, 73 | update_desktop_database: true, 74 | ) 75 | -------------------------------------------------------------------------------- /src/view/window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67 | 68 | -------------------------------------------------------------------------------- /src/view/volume.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use gettextrs::gettext; 3 | use glib::clone::Downgrade; 4 | use gtk::glib; 5 | 6 | use crate::model; 7 | use crate::utils; 8 | use crate::view; 9 | 10 | pub(crate) async fn delete_volume_show_confirmation(widget: &W, volume: Option<&model::Volume>) 11 | where 12 | W: IsA + Downgrade>, 13 | { 14 | let volume = if let Some(volume) = volume { 15 | volume 16 | } else { 17 | return; 18 | }; 19 | 20 | match volume.container_list().get(0) { 21 | Some(container) => { 22 | let dialog = adw::AlertDialog::builder() 23 | .heading(gettext("Confirm Volume Deletion")) 24 | .body_use_markup(true) 25 | .body(gettext!( 26 | // Translators: The "{}" is a placeholder for the container name. 27 | "Volume is used by container {}. Deleting the volume will also delete these containers.", 28 | container.name(), 29 | )) 30 | .build(); 31 | 32 | dialog.add_responses(&[ 33 | ("cancel", &gettext("_Cancel")), 34 | ("delete", &gettext("_Delete")), 35 | ]); 36 | dialog.set_default_response(Some("cancel")); 37 | dialog.set_response_appearance("delete", adw::ResponseAppearance::Destructive); 38 | 39 | if "delete" == dialog.choose_future(widget).await { 40 | delete_volume(widget, volume, true).await; 41 | } 42 | } 43 | None => delete_volume(widget, volume, false).await, 44 | } 45 | } 46 | 47 | async fn delete_volume(widget: &W, volume: &model::Volume, force: bool) 48 | where 49 | W: IsA + Downgrade>, 50 | { 51 | if let Err(e) = volume.delete(force).await { 52 | utils::show_error_toast( 53 | widget, 54 | // Translators: The "{}" is a placeholder for the volume name. 55 | &gettext!("Error on deleting volume '{}'", &volume.inner().name), 56 | &e.to_string(), 57 | ); 58 | } 59 | } 60 | 61 | pub(crate) fn create_container>(widget: &W, volume: Option) { 62 | if let Some(volume) = volume { 63 | utils::Dialog::new(widget, &view::ContainerCreationPage::from(&volume)).present(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/view/images_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | 75 | -------------------------------------------------------------------------------- /src/view/volumes_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | 75 | -------------------------------------------------------------------------------- /src/view/container_menu_button.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | _Start 8 | container-menu-button.start 9 | action-disabled 10 | 11 | 12 | S_top 13 | container-menu-button.stop 14 | action-disabled 15 | 16 | 17 | R_estart 18 | container-menu-button.restart 19 | action-disabled 20 | 21 | 22 | _Resume 23 | container-menu-button.resume 24 | action-disabled 25 | 26 | 27 | _Pause 28 | container-menu-button.pause 29 | action-disabled 30 | 31 |
32 |
33 | 34 | Re_name… 35 | container-menu-button.rename 36 | action-disabled 37 | 38 |
39 |
40 | 41 | _Delete 42 | container-menu-button.delete 43 | action-disabled 44 | 45 |
46 |
47 | 48 | 61 |
62 | -------------------------------------------------------------------------------- /src/model/health_check_log_list.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::VecDeque; 3 | 4 | use gio::prelude::*; 5 | use gio::subclass::prelude::*; 6 | use gtk::gio; 7 | use gtk::glib; 8 | 9 | use crate::model; 10 | use crate::podman; 11 | 12 | mod imp { 13 | use super::*; 14 | 15 | #[derive(Debug, Default)] 16 | pub(crate) struct HealthCheckLogList { 17 | pub(super) list: RefCell>, 18 | } 19 | 20 | #[glib::object_subclass] 21 | impl ObjectSubclass for HealthCheckLogList { 22 | const NAME: &'static str = "HealthCheckLogList"; 23 | type Type = super::HealthCheckLogList; 24 | type Interfaces = (gio::ListModel,); 25 | } 26 | 27 | impl ObjectImpl for HealthCheckLogList {} 28 | 29 | impl ListModelImpl for HealthCheckLogList { 30 | fn item_type(&self) -> glib::Type { 31 | model::HealthCheckLog::static_type() 32 | } 33 | 34 | fn n_items(&self) -> u32 { 35 | self.list.borrow().len() as u32 36 | } 37 | 38 | fn item(&self, position: u32) -> Option { 39 | self.list 40 | .borrow() 41 | .get(position as usize) 42 | .map(|obj| obj.upcast_ref()) 43 | .cloned() 44 | } 45 | } 46 | } 47 | 48 | glib::wrapper! { 49 | pub(crate) struct HealthCheckLogList(ObjectSubclass) 50 | @implements gio::ListModel, model::SelectableList; 51 | } 52 | 53 | impl Default for HealthCheckLogList { 54 | fn default() -> Self { 55 | glib::Object::builder().build() 56 | } 57 | } 58 | 59 | impl HealthCheckLogList { 60 | pub(crate) fn sync(&self, logs: &[podman::models::HealthCheckLog]) { 61 | let mut list = self.imp().list.borrow_mut(); 62 | 63 | let len_old = list.len(); 64 | 65 | let first = logs.first().and_then(|log| log.start.as_deref()); 66 | while list.front().is_some() && list.front().map(|log| log.start()).as_deref() != first { 67 | list.pop_front(); 68 | } 69 | 70 | let len_removed = list.len(); 71 | let num_removed = len_old - len_removed; 72 | let num_added = logs.len() - list.len(); 73 | 74 | logs[list.len()..].iter().for_each(|log| { 75 | list.push_back(model::HealthCheckLog::from(log)); 76 | }); 77 | 78 | drop(list); 79 | 80 | self.items_changed(0, num_removed as u32, 0); 81 | self.items_changed(len_removed as u32, 0, num_added as u32); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/model/image_config.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | 3 | use glib::Properties; 4 | use glib::prelude::*; 5 | use glib::subclass::prelude::*; 6 | use gtk::glib; 7 | 8 | use crate::podman; 9 | use crate::utils; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties)] 15 | #[properties(wrapper_type = super::ImageConfig)] 16 | pub(crate) struct ImageConfig { 17 | #[property(get, set, construct_only, nullable)] 18 | pub(super) cmd: OnceCell>, 19 | #[property(get, set, construct_only, nullable)] 20 | pub(super) entrypoint: OnceCell>, 21 | #[property(get, set, construct_only)] 22 | pub(super) exposed_ports: OnceCell, 23 | } 24 | 25 | #[glib::object_subclass] 26 | impl ObjectSubclass for ImageConfig { 27 | const NAME: &'static str = "ImageConfig"; 28 | type Type = super::ImageConfig; 29 | } 30 | 31 | impl ObjectImpl for ImageConfig { 32 | fn properties() -> &'static [glib::ParamSpec] { 33 | Self::derived_properties() 34 | } 35 | 36 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 37 | self.derived_set_property(id, value, pspec); 38 | } 39 | 40 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 41 | self.derived_property(id, pspec) 42 | } 43 | } 44 | } 45 | 46 | glib::wrapper! { 47 | pub(crate) struct ImageConfig(ObjectSubclass); 48 | } 49 | 50 | impl ImageConfig { 51 | pub(crate) fn from_libpod(config: &podman::models::ImageConfig) -> Self { 52 | glib::Object::builder() 53 | .property( 54 | "cmd", 55 | utils::format_iter_or_none(config.cmd.as_deref().unwrap_or_default().iter(), " "), 56 | ) 57 | .property( 58 | "entrypoint", 59 | utils::format_iter_or_none( 60 | config.entrypoint.as_deref().unwrap_or_default().iter(), 61 | " ", 62 | ), 63 | ) 64 | .property( 65 | "exposed-ports", 66 | gtk::StringList::new( 67 | &config 68 | .exposed_ports 69 | .as_ref() 70 | .map(|ports| ports.keys().map(String::as_str).collect::>()) 71 | .unwrap_or_default(), 72 | ), 73 | ) 74 | .build() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/view/repo_tag_add_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 66 | 67 | -------------------------------------------------------------------------------- /src/widget/text_search_entry.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 68 | 69 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | # UI resources 2 | ui_resources = gnome.compile_resources( 3 | 'ui-resources', 4 | 'resources.gresource.xml', 5 | gresource_bundle: true, 6 | source_dir: meson.current_build_dir(), 7 | install: true, 8 | install_dir: pkgdatadir, 9 | ) 10 | 11 | global_conf = configuration_data() 12 | global_conf.set_quoted('APP_ID', application_id) 13 | global_conf.set_quoted('PKGDATADIR', pkgdatadir) 14 | global_conf.set_quoted('PROFILE', profile) 15 | global_conf.set_quoted('VERSION', version + version_suffix) 16 | global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) 17 | global_conf.set_quoted('LOCALEDIR', localedir) 18 | config = configure_file( 19 | input: 'config.rs.in', 20 | output: 'config.rs', 21 | configuration: global_conf 22 | ) 23 | # Copy the config.rs output to the source directory. 24 | run_command( 25 | 'cp', 26 | meson.project_build_root() / 'src' / 'config.rs', 27 | meson.project_source_root() / 'src' / 'config.rs', 28 | check: true 29 | ) 30 | 31 | cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] 32 | cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ] 33 | 34 | if get_option('profile') == 'default' 35 | cargo_options += [ '--release' ] 36 | rust_target = 'release' 37 | message('Building in release mode') 38 | else 39 | rust_target = 'debug' 40 | message('Building in debug mode') 41 | endif 42 | 43 | cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] 44 | 45 | cargo_build = custom_target( 46 | 'cargo-build', 47 | build_by_default: true, 48 | build_always_stale: true, 49 | output: meson.project_name(), 50 | console: true, 51 | install: true, 52 | install_dir: bindir, 53 | depends: resources, 54 | command: [ 55 | 'env', 56 | cargo_env, 57 | cargo, 'build', 58 | cargo_options, 59 | '&&', 60 | 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', 61 | ] 62 | ) 63 | 64 | cargo = find_program('cargo', required: true) 65 | cargo_target_dir = meson.project_build_root() / 'target' 66 | cargo_home = meson.project_build_root() / 'cargo-home' 67 | manifest_path = meson.project_source_root() / 'Cargo.toml' 68 | test ( 69 | 'clippy', 70 | cargo, 71 | args: [ 72 | 'clippy', 73 | '--manifest-path=@0@'.format(manifest_path), 74 | '--target-dir=@0@'.format(cargo_target_dir), 75 | '--', 76 | '-D', 77 | 'warnings', 78 | ], 79 | env: [ 80 | 'CARGO_HOME=@0@'.format(cargo_home), 81 | 'PATH=/app/bin:/usr/bin:/usr/lib/sdk/llvm21/bin:/usr/lib/sdk/rust-stable/bin', 82 | ], 83 | timeout: 300, # Give cargo more time 84 | ) 85 | -------------------------------------------------------------------------------- /src/model/device.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::cell::RefCell; 3 | use std::sync::OnceLock; 4 | 5 | use glib::Properties; 6 | use glib::prelude::*; 7 | use glib::subclass::Signal; 8 | use glib::subclass::prelude::*; 9 | use gtk::glib; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties)] 15 | #[properties(wrapper_type = super::Device)] 16 | pub(crate) struct Device { 17 | #[property(get, set)] 18 | pub(super) host_path: RefCell, 19 | #[property(get, set)] 20 | pub(super) container_path: RefCell, 21 | #[property(get, set)] 22 | pub(super) writable: Cell, 23 | #[property(get, set)] 24 | pub(super) readable: Cell, 25 | #[property(get, set)] 26 | pub(super) mknod: Cell, 27 | } 28 | 29 | #[glib::object_subclass] 30 | impl ObjectSubclass for Device { 31 | const NAME: &'static str = "Device"; 32 | type Type = super::Device; 33 | } 34 | 35 | impl ObjectImpl for Device { 36 | fn signals() -> &'static [Signal] { 37 | static SIGNALS: OnceLock> = OnceLock::new(); 38 | SIGNALS.get_or_init(|| vec![Signal::builder("remove-request").build()]) 39 | } 40 | 41 | fn properties() -> &'static [glib::ParamSpec] { 42 | Self::derived_properties() 43 | } 44 | 45 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 46 | self.derived_set_property(id, value, pspec); 47 | } 48 | 49 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 50 | self.derived_property(id, pspec) 51 | } 52 | } 53 | } 54 | 55 | glib::wrapper! { 56 | pub(crate) struct Device(ObjectSubclass); 57 | } 58 | 59 | impl Default for Device { 60 | fn default() -> Self { 61 | glib::Object::builder() 62 | .property("readable", true) 63 | .property("writable", false) 64 | .property("mknod", false) 65 | .build() 66 | } 67 | } 68 | 69 | impl Device { 70 | pub(crate) fn remove_request(&self) { 71 | self.emit_by_name::<()>("remove-request", &[]); 72 | } 73 | 74 | pub(crate) fn connect_remove_request( 75 | &self, 76 | f: F, 77 | ) -> glib::SignalHandlerId { 78 | self.connect_local("remove-request", true, move |values| { 79 | let obj = values[0].get::().unwrap(); 80 | f(&obj); 81 | 82 | None 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/view/containers_count_bar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 86 | 87 | -------------------------------------------------------------------------------- /src/model/image_search_response.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | 3 | use glib::Properties; 4 | use glib::prelude::*; 5 | use glib::subclass::prelude::*; 6 | use gtk::glib; 7 | 8 | use crate::podman; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | #[derive(Debug, Default, Properties)] 14 | #[properties(wrapper_type = super::ImageSearchResponse)] 15 | pub(crate) struct ImageSearchResponse { 16 | #[property(get, set, construct_only, nullable)] 17 | pub(super) automated: OnceCell>, 18 | #[property(get, set, construct_only, nullable)] 19 | pub(super) description: OnceCell>, 20 | #[property(get, set, construct_only, nullable)] 21 | pub(super) index: OnceCell>, 22 | #[property(get, set, construct_only, nullable)] 23 | pub(super) name: OnceCell>, 24 | #[property(get, set, construct_only, nullable)] 25 | pub(super) official: OnceCell, 26 | #[property(get, set, construct_only)] 27 | pub(super) stars: OnceCell, 28 | #[property(get, set, construct_only, nullable)] 29 | pub(super) tag: OnceCell>, 30 | } 31 | 32 | #[glib::object_subclass] 33 | impl ObjectSubclass for ImageSearchResponse { 34 | const NAME: &'static str = "ImageSearchResponse"; 35 | type Type = super::ImageSearchResponse; 36 | } 37 | 38 | impl ObjectImpl for ImageSearchResponse { 39 | fn properties() -> &'static [glib::ParamSpec] { 40 | Self::derived_properties() 41 | } 42 | 43 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 44 | self.derived_set_property(id, value, pspec); 45 | } 46 | 47 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 48 | self.derived_property(id, pspec) 49 | } 50 | } 51 | } 52 | 53 | glib::wrapper! { 54 | pub(crate) struct ImageSearchResponse(ObjectSubclass); 55 | } 56 | 57 | impl From for ImageSearchResponse { 58 | fn from(response: podman::models::RegistrySearchResponse) -> Self { 59 | glib::Object::builder() 60 | .property("automated", response.automated) 61 | .property("description", response.description) 62 | .property("index", response.index) 63 | .property("name", response.name) 64 | .property( 65 | "official", 66 | response.official.map(|s| !s.is_empty()).unwrap_or(false), 67 | ) 68 | .property("stars", response.stars.unwrap_or(-1)) 69 | .property("tag", response.tag) 70 | .build() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('icons') 2 | subdir('resources') 3 | # Desktop file 4 | desktop_conf = configuration_data() 5 | desktop_conf.set('icon', application_id) 6 | desktop_file = i18n.merge_file( 7 | type: 'desktop', 8 | input: configure_file( 9 | input: '@0@.desktop.in.in'.format(base_id), 10 | output: '@BASENAME@', 11 | configuration: desktop_conf 12 | ), 13 | output: '@0@.desktop'.format(application_id), 14 | po_dir: podir, 15 | install: true, 16 | install_dir: datadir / 'applications' 17 | ) 18 | # Validate Desktop file 19 | if desktop_file_validate.found() 20 | test( 21 | 'validate-desktop', 22 | desktop_file_validate, 23 | args: [ 24 | desktop_file.full_path() 25 | ], 26 | depends: desktop_file, 27 | ) 28 | endif 29 | 30 | # Appdata 31 | appdata_conf = configuration_data() 32 | appdata_conf.set('app-id', application_id) 33 | appdata_conf.set('gettext-package', gettext_package) 34 | appdata_file = i18n.merge_file( 35 | input: configure_file( 36 | input: '@0@.metainfo.xml.in.in'.format(base_id), 37 | output: '@BASENAME@', 38 | configuration: appdata_conf 39 | ), 40 | output: '@0@.metainfo.xml'.format(application_id), 41 | po_dir: podir, 42 | install: true, 43 | install_dir: datadir / 'metainfo' 44 | ) 45 | # Validate Appdata 46 | if appstream_util.found() 47 | test( 48 | 'validate-appdata', appstream_util, 49 | args: [ 50 | 'validate', '--nonet', appdata_file.full_path() 51 | ], 52 | depends: appdata_file, 53 | ) 54 | endif 55 | 56 | # GSchema 57 | gschema_conf = configuration_data() 58 | gschema_conf.set('app-id', application_id) 59 | gschema_conf.set('gettext-package', gettext_package) 60 | configure_file( 61 | input: '@0@.gschema.xml.in'.format(base_id), 62 | output: '@0@.gschema.xml'.format(application_id), 63 | configuration: gschema_conf, 64 | install: true, 65 | install_dir: datadir / 'glib-2.0' / 'schemas' 66 | ) 67 | 68 | # Validate GSchema 69 | if glib_compile_schemas.found() 70 | test( 71 | 'validate-gschema', glib_compile_schemas, 72 | args: [ 73 | '--strict', '--dry-run', meson.current_build_dir() 74 | ], 75 | ) 76 | endif 77 | 78 | # Resources 79 | resources_conf = configuration_data() 80 | resources_conf.set('app-id', application_id) 81 | appdata_resource = configure_file( 82 | input: 'resources.gresource.xml.in', 83 | output: 'resources.gresource.xml', 84 | configuration: resources_conf, 85 | install: false, 86 | ) 87 | resources = gnome.compile_resources( 88 | 'appdata-resources', 89 | '@0@/resources.gresource.xml'.format(meson.current_build_dir()), 90 | gresource_bundle: true, 91 | source_dir: meson.current_build_dir(), 92 | install: true, 93 | install_dir: pkgdatadir, 94 | dependencies: [appdata_file, appdata_resource], 95 | ) 96 | -------------------------------------------------------------------------------- /src/view/container_terminal_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | zoom-control 7 | 8 | 9 | 10 | 11 | container-terminal-page.zoom-in 12 | container-terminal-page.zoom-normal 13 | container-terminal-page.zoom-out 14 | 15 | 16 | 17 | 70 | 71 | -------------------------------------------------------------------------------- /src/view/device_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 92 | 93 | -------------------------------------------------------------------------------- /src/view/pods_prune_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | horizontal 6 | 7 | 8 | 9 | 10 | 11 | 12 | 80 | 81 | -------------------------------------------------------------------------------- /src/view/connections_sidebar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 80 | 81 | -------------------------------------------------------------------------------- /src/view/pods_prune_page.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::utils; 9 | use crate::view; 10 | 11 | const ACTION_PRUNE: &str = "pods-prune-page.prune"; 12 | 13 | mod imp { 14 | use super::*; 15 | 16 | #[derive(Debug, Default, Properties, CompositeTemplate)] 17 | #[properties(wrapper_type = super::PodsPrunePage)] 18 | #[template(resource = "/com/github/marhkb/Pods/ui/view/pods_prune_page.ui")] 19 | pub(crate) struct PodsPrunePage { 20 | #[property(get, set, construct_only, nullable)] 21 | pub(super) client: glib::WeakRef, 22 | #[template_child] 23 | pub(super) navigation_view: TemplateChild, 24 | } 25 | 26 | #[glib::object_subclass] 27 | impl ObjectSubclass for PodsPrunePage { 28 | const NAME: &'static str = "PdsPodsPrunePage"; 29 | type Type = super::PodsPrunePage; 30 | type ParentType = gtk::Widget; 31 | 32 | fn class_init(klass: &mut Self::Class) { 33 | klass.bind_template(); 34 | 35 | klass.install_action(ACTION_PRUNE, None, |widget, _, _| { 36 | widget.prune(); 37 | }); 38 | } 39 | 40 | fn instance_init(obj: &glib::subclass::InitializingObject) { 41 | obj.init_template(); 42 | } 43 | } 44 | 45 | impl ObjectImpl for PodsPrunePage { 46 | fn properties() -> &'static [glib::ParamSpec] { 47 | Self::derived_properties() 48 | } 49 | 50 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 51 | self.derived_set_property(id, value, pspec); 52 | } 53 | 54 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 55 | self.derived_property(id, pspec) 56 | } 57 | 58 | fn dispose(&self) { 59 | utils::unparent_children(&*self.obj()); 60 | } 61 | } 62 | 63 | impl WidgetImpl for PodsPrunePage {} 64 | } 65 | 66 | glib::wrapper! { 67 | pub(crate) struct PodsPrunePage(ObjectSubclass) 68 | @extends gtk::Widget, 69 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 70 | } 71 | 72 | impl From<&model::Client> for PodsPrunePage { 73 | fn from(client: &model::Client) -> Self { 74 | glib::Object::builder().property("client", client).build() 75 | } 76 | } 77 | 78 | impl PodsPrunePage { 79 | pub(crate) fn prune(&self) { 80 | let imp = self.imp(); 81 | 82 | let action = self.client().unwrap().action_list().prune_pods(); 83 | let page = view::ActionPage::from(&action); 84 | 85 | imp.navigation_view.push( 86 | &adw::NavigationPage::builder() 87 | .can_pop(false) 88 | .child(&page) 89 | .build(), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/view/image.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use gettextrs::gettext; 3 | use glib::clone; 4 | use glib::clone::Downgrade; 5 | use gtk::gio; 6 | use gtk::glib; 7 | 8 | use crate::model; 9 | use crate::utils; 10 | use crate::view; 11 | 12 | pub(crate) fn delete_image_show_confirmation(widget: &W, image: Option) 13 | where 14 | W: IsA + Downgrade>, 15 | { 16 | if let Some(image) = image { 17 | match image.container_list().get(0) { 18 | Some(container) => { 19 | let dialog = adw::AlertDialog::builder() 20 | .heading(gettext("Confirm Image Deletion")) 21 | .body_use_markup(true) 22 | .body(gettext!( 23 | // Translators: The "{}" is a placeholder for the container name. 24 | "Image is used by container {}. Deleting the image will also delete all its associated containers.", 25 | container.name(), 26 | )) 27 | .build(); 28 | 29 | dialog.add_responses(&[ 30 | ("cancel", &gettext("_Cancel")), 31 | ("delete", &gettext("_Delete")), 32 | ]); 33 | dialog.set_default_response(Some("cancel")); 34 | dialog.set_response_appearance("delete", adw::ResponseAppearance::Destructive); 35 | 36 | dialog.choose( 37 | widget, 38 | gio::Cancellable::NONE, 39 | clone!( 40 | #[weak] 41 | widget, 42 | #[weak] 43 | image, 44 | move |response| { 45 | if response == "delete" { 46 | delete_image(&widget, &image); 47 | } 48 | } 49 | ), 50 | ); 51 | } 52 | None => delete_image(widget, &image), 53 | } 54 | } 55 | } 56 | 57 | pub(crate) fn delete_image(widget: &W, image: &model::Image) 58 | where 59 | W: IsA + Downgrade>, 60 | { 61 | image.delete(clone!( 62 | #[weak] 63 | widget, 64 | move |image, result| { 65 | if let Err(e) = result { 66 | utils::show_error_toast( 67 | &widget, 68 | // Translators: The "{}" is a placeholder for the image id. 69 | &gettext!("Error on deleting image '{}'", image.id()), 70 | &e.to_string(), 71 | ); 72 | } 73 | } 74 | )); 75 | } 76 | 77 | pub(crate) fn create_container>(widget: &W, image: Option) { 78 | if let Some(image) = image { 79 | utils::Dialog::new(widget, &view::ContainerCreationPage::from(&image)).present(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/model/port_mapping_list.rs: -------------------------------------------------------------------------------- 1 | use std::cell::OnceCell; 2 | use std::sync::OnceLock; 3 | 4 | use gio::prelude::*; 5 | use gio::subclass::prelude::*; 6 | use gtk::gio; 7 | use gtk::glib; 8 | 9 | use crate::model; 10 | use crate::podman; 11 | 12 | mod imp { 13 | use super::*; 14 | 15 | #[derive(Debug, Default)] 16 | pub(crate) struct PortMappingList { 17 | pub(super) list: OnceCell>, 18 | } 19 | 20 | #[glib::object_subclass] 21 | impl ObjectSubclass for PortMappingList { 22 | const NAME: &'static str = "PortMappingList"; 23 | type Type = super::PortMappingList; 24 | type Interfaces = (gio::ListModel,); 25 | } 26 | 27 | impl ObjectImpl for PortMappingList { 28 | fn properties() -> &'static [glib::ParamSpec] { 29 | static PROPERTIES: OnceLock> = OnceLock::new(); 30 | PROPERTIES.get_or_init(|| { 31 | vec![ 32 | glib::ParamSpecUInt::builder("len") 33 | .explicit_notify() 34 | .build(), 35 | ] 36 | }) 37 | } 38 | 39 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { 40 | match pspec.name() { 41 | "len" => self.obj().len().to_value(), 42 | _ => unimplemented!(), 43 | } 44 | } 45 | 46 | fn constructed(&self) { 47 | self.parent_constructed(); 48 | self.obj() 49 | .connect_items_changed(|obj, _, _, _| obj.notify("len")); 50 | } 51 | } 52 | 53 | impl ListModelImpl for PortMappingList { 54 | fn item_type(&self) -> glib::Type { 55 | model::PortMapping::static_type() 56 | } 57 | 58 | fn n_items(&self) -> u32 { 59 | self.obj().len() 60 | } 61 | 62 | fn item(&self, position: u32) -> Option { 63 | self.obj().get(position as usize).map(|obj| obj.upcast()) 64 | } 65 | } 66 | } 67 | 68 | glib::wrapper! { 69 | pub(crate) struct PortMappingList(ObjectSubclass) 70 | @implements gio::ListModel; 71 | } 72 | 73 | impl From> for PortMappingList { 74 | fn from(port_mappings: Vec) -> Self { 75 | let obj: Self = glib::Object::builder().build(); 76 | obj.imp() 77 | .list 78 | .set( 79 | port_mappings 80 | .into_iter() 81 | .map(model::PortMapping::from) 82 | .collect(), 83 | ) 84 | .unwrap(); 85 | obj 86 | } 87 | } 88 | 89 | impl PortMappingList { 90 | pub(crate) fn get(&self, index: usize) -> Option { 91 | self.imp().list.get().unwrap().get(index).cloned() 92 | } 93 | 94 | pub(crate) fn len(&self) -> u32 { 95 | self.imp().list.get().unwrap().len() as u32 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/widget/random_name_entry_row.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use adw::prelude::*; 4 | use adw::subclass::prelude::*; 5 | use gtk::CompositeTemplate; 6 | use gtk::glib; 7 | 8 | mod imp { 9 | use super::*; 10 | 11 | #[derive(Default, CompositeTemplate)] 12 | #[template(resource = "/com/github/marhkb/Pods/ui/widget/random_name_entry_row.ui")] 13 | pub(crate) struct RandomNameEntryRow { 14 | pub(super) names: RefCell>, 15 | #[template_child] 16 | pub(super) generate_button: TemplateChild, 17 | } 18 | 19 | #[glib::object_subclass] 20 | impl ObjectSubclass for RandomNameEntryRow { 21 | const NAME: &'static str = "PdsRandomNameEntryRow"; 22 | type Type = super::RandomNameEntryRow; 23 | type ParentType = adw::EntryRow; 24 | type Interfaces = (gtk::Editable,); 25 | 26 | fn class_init(klass: &mut Self::Class) { 27 | klass.bind_template(); 28 | 29 | klass.install_action( 30 | "random-name-entry-row.generate", 31 | None, 32 | move |widget, _, _| { 33 | widget.generate_random_name(); 34 | }, 35 | ); 36 | } 37 | 38 | fn instance_init(obj: &glib::subclass::InitializingObject) { 39 | obj.init_template(); 40 | } 41 | } 42 | 43 | impl ObjectImpl for RandomNameEntryRow { 44 | fn set_property(&self, _: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 45 | match pspec.name() { 46 | "text" => self.obj().set_text(value.get().unwrap()), 47 | _ => unimplemented!(), 48 | } 49 | } 50 | 51 | fn property(&self, _: usize, pspec: &glib::ParamSpec) -> glib::Value { 52 | match pspec.name() { 53 | "text" => self.obj().text().to_value(), 54 | _ => unimplemented!(), 55 | } 56 | } 57 | 58 | fn constructed(&self) { 59 | self.parent_constructed(); 60 | self.obj().generate_random_name() 61 | } 62 | } 63 | 64 | impl WidgetImpl for RandomNameEntryRow {} 65 | impl ListBoxRowImpl for RandomNameEntryRow {} 66 | impl PreferencesRowImpl for RandomNameEntryRow {} 67 | impl EntryRowImpl for RandomNameEntryRow {} 68 | impl EditableImpl for RandomNameEntryRow {} 69 | } 70 | 71 | glib::wrapper! { 72 | pub(crate) struct RandomNameEntryRow(ObjectSubclass) 73 | @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::EntryRow, 74 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget, gtk::Editable; 75 | } 76 | 77 | impl Default for RandomNameEntryRow { 78 | fn default() -> Self { 79 | glib::Object::builder().build() 80 | } 81 | } 82 | 83 | impl RandomNameEntryRow { 84 | pub(crate) fn generate_random_name(&self) { 85 | self.set_text(&self.imp().names.borrow_mut().next().unwrap()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/view/info_row.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use glib::closure; 5 | use gtk::CompositeTemplate; 6 | use gtk::glib; 7 | 8 | use crate::model; 9 | use crate::utils; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties, CompositeTemplate)] 15 | #[properties(wrapper_type = super::InfoRow)] 16 | #[template(resource = "/com/github/marhkb/Pods/ui/view/info_row.ui")] 17 | pub(crate) struct InfoRow { 18 | #[property(get, set)] 19 | pub(super) client: glib::WeakRef, 20 | #[template_child] 21 | pub(super) version_stack: TemplateChild, 22 | #[template_child] 23 | pub(super) version_label: TemplateChild, 24 | } 25 | 26 | #[glib::object_subclass] 27 | impl ObjectSubclass for InfoRow { 28 | const NAME: &'static str = "PdsInfoRow"; 29 | type Type = super::InfoRow; 30 | type ParentType = gtk::Widget; 31 | 32 | fn class_init(klass: &mut Self::Class) { 33 | klass.bind_template(); 34 | } 35 | 36 | fn instance_init(obj: &glib::subclass::InitializingObject) { 37 | obj.init_template(); 38 | } 39 | } 40 | 41 | impl ObjectImpl for InfoRow { 42 | fn properties() -> &'static [glib::ParamSpec] { 43 | Self::derived_properties() 44 | } 45 | 46 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 47 | self.derived_set_property(id, value, pspec); 48 | } 49 | 50 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 51 | self.derived_property(id, pspec) 52 | } 53 | 54 | fn constructed(&self) { 55 | self.parent_constructed(); 56 | 57 | let obj = &*self.obj(); 58 | 59 | let version_expr = 60 | Self::Type::this_expression("client").chain_property::("version"); 61 | 62 | version_expr 63 | .chain_closure::(closure!(|_: Self::Type, version: Option<&str>| version 64 | .map(|_| "version") 65 | .unwrap_or("loading"))) 66 | .bind(&*self.version_stack, "visible-child-name", Some(obj)); 67 | 68 | version_expr 69 | .chain_closure::(closure!(|_: Self::Type, version: Option<&str>| { 70 | version 71 | .map(|version| format!("v{version}")) 72 | .unwrap_or_default() 73 | })) 74 | .bind(&*self.version_label, "label", Some(obj)); 75 | } 76 | 77 | fn dispose(&self) { 78 | utils::unparent_children(&*self.obj()); 79 | } 80 | } 81 | 82 | impl WidgetImpl for InfoRow {} 83 | } 84 | 85 | glib::wrapper! { 86 | pub(crate) struct InfoRow(ObjectSubclass) 87 | @extends gtk::Widget, 88 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 89 | } 90 | -------------------------------------------------------------------------------- /src/widget/scalable_text_view.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | 3 | use adw::prelude::*; 4 | use adw::subclass::prelude::*; 5 | use glib::Properties; 6 | use gtk::glib; 7 | use sourceview5::subclass::view::ViewImpl; 8 | 9 | use crate::utils::PodsSettings; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties)] 15 | #[properties(wrapper_type = super::ScalableTextView)] 16 | pub(crate) struct ScalableTextView { 17 | pub(super) settings: PodsSettings, 18 | pub(super) css_provider: gtk::CssProvider, 19 | #[property(get, set, minimum = 0.1, lax_validation, default = 1.0)] 20 | pub(super) font_scale: Cell, 21 | } 22 | 23 | #[glib::object_subclass] 24 | impl ObjectSubclass for ScalableTextView { 25 | const NAME: &'static str = "PdsScalableTextView"; 26 | type Type = super::ScalableTextView; 27 | type ParentType = sourceview5::View; 28 | } 29 | 30 | impl ObjectImpl for ScalableTextView { 31 | fn properties() -> &'static [glib::ParamSpec] { 32 | Self::derived_properties() 33 | } 34 | 35 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 36 | self.derived_set_property(id, value, pspec); 37 | } 38 | 39 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 40 | self.derived_property(id, pspec) 41 | } 42 | 43 | fn constructed(&self) { 44 | self.parent_constructed(); 45 | 46 | let obj = &*self.obj(); 47 | 48 | obj.style_context() 49 | .add_provider(&self.css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); 50 | obj.connect_notify_local(Some("font-scale"), |obj, _| { 51 | obj.update_css(); 52 | }); 53 | 54 | self.settings 55 | .bind("text-view-font-scale", obj, "font-scale") 56 | .build(); 57 | } 58 | } 59 | 60 | impl WidgetImpl for ScalableTextView {} 61 | impl TextViewImpl for ScalableTextView {} 62 | impl ViewImpl for ScalableTextView {} 63 | } 64 | 65 | glib::wrapper! { 66 | pub(crate) struct ScalableTextView(ObjectSubclass) 67 | @extends gtk::Widget, gtk::TextView, sourceview5::View, 68 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Scrollable; 69 | } 70 | 71 | impl Default for ScalableTextView { 72 | fn default() -> Self { 73 | glib::Object::builder().build() 74 | } 75 | } 76 | 77 | impl ScalableTextView { 78 | pub(crate) fn zoom_in(&self) { 79 | self.set_font_scale(self.font_scale() + 0.1); 80 | } 81 | 82 | pub(crate) fn zoom_out(&self) { 83 | self.set_font_scale(self.font_scale() - 0.1); 84 | } 85 | 86 | pub(crate) fn zoom_normal(&self) { 87 | self.set_font_scale(1.0); 88 | } 89 | 90 | fn update_css(&self) { 91 | self.imp().css_provider.load_from_data(&format!( 92 | "textview {{ font-size: {}em; }}", 93 | self.font_scale() 94 | )); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/view/pods_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 93 | 94 | -------------------------------------------------------------------------------- /src/view/image_search_response_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 90 | 91 | -------------------------------------------------------------------------------- /data/resources/icons/scalable/status/verified-checkmark-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/view/containers_row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 93 | 94 | -------------------------------------------------------------------------------- /src/view/volume_creation_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | horizontal 6 | 7 | 8 | 9 | 10 | 11 | 12 | 89 | 90 | -------------------------------------------------------------------------------- /src/view/image_pull_page.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::podman; 9 | use crate::utils; 10 | use crate::view; 11 | 12 | mod imp { 13 | use super::*; 14 | 15 | #[derive(Debug, Default, Properties, CompositeTemplate)] 16 | #[properties(wrapper_type = super::ImagePullPage)] 17 | #[template(resource = "/com/github/marhkb/Pods/ui/view/image_pull_page.ui")] 18 | pub(crate) struct ImagePullPage { 19 | #[property(get, set, construct_only, nullable)] 20 | pub(super) client: glib::WeakRef, 21 | #[template_child] 22 | pub(super) navigation_view: TemplateChild, 23 | } 24 | 25 | #[glib::object_subclass] 26 | impl ObjectSubclass for ImagePullPage { 27 | const NAME: &'static str = "PdsImagePullPage"; 28 | type Type = super::ImagePullPage; 29 | type ParentType = gtk::Widget; 30 | 31 | fn class_init(klass: &mut Self::Class) { 32 | klass.bind_template(); 33 | klass.bind_template_callbacks(); 34 | } 35 | 36 | fn instance_init(obj: &glib::subclass::InitializingObject) { 37 | obj.init_template(); 38 | } 39 | } 40 | 41 | impl ObjectImpl for ImagePullPage { 42 | fn properties() -> &'static [glib::ParamSpec] { 43 | Self::derived_properties() 44 | } 45 | 46 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 47 | self.derived_set_property(id, value, pspec); 48 | } 49 | 50 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 51 | self.derived_property(id, pspec) 52 | } 53 | 54 | fn dispose(&self) { 55 | utils::unparent_children(&*self.obj()); 56 | } 57 | } 58 | 59 | impl WidgetImpl for ImagePullPage {} 60 | 61 | #[gtk::template_callbacks] 62 | impl ImagePullPage { 63 | #[template_callback] 64 | fn on_image_selected(&self, image: &str) { 65 | let opts = podman::opts::PullOpts::builder() 66 | .reference(image) 67 | .quiet(false) 68 | .build(); 69 | 70 | let page = view::ActionPage::from( 71 | &self 72 | .obj() 73 | .client() 74 | .unwrap() 75 | .action_list() 76 | .download_image(image, opts), 77 | ); 78 | 79 | self.navigation_view.push( 80 | &adw::NavigationPage::builder() 81 | .can_pop(false) 82 | .child(&page) 83 | .build(), 84 | ); 85 | } 86 | } 87 | } 88 | 89 | glib::wrapper! { 90 | pub(crate) struct ImagePullPage(ObjectSubclass) 91 | @extends gtk::Widget, 92 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 93 | } 94 | 95 | impl From<&model::Client> for ImagePullPage { 96 | fn from(client: &model::Client) -> Self { 97 | glib::Object::builder().property("client", client).build() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/view/containers_prune_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | horizontal 6 | 7 | 8 | 9 | 10 | 11 | 12 | 89 | 90 | -------------------------------------------------------------------------------- /src/view/volumes_prune_page.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | horizontal 6 | 7 | 8 | 9 | 10 | 11 | 12 | 90 | 91 | -------------------------------------------------------------------------------- /src/view/containers_list_view.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::gio; 6 | use gtk::glib; 7 | 8 | use crate::utils; 9 | use crate::view; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(Debug, Default, Properties, CompositeTemplate)] 15 | #[properties(wrapper_type = super::ContainersListView)] 16 | #[template(resource = "/com/github/marhkb/Pods/ui/view/containers_list_view.ui")] 17 | pub(crate) struct ContainersListView { 18 | #[property(get, set = Self::set_model, nullable, construct)] 19 | pub(super) model: glib::WeakRef, 20 | #[template_child] 21 | pub(super) list_box: TemplateChild, 22 | } 23 | 24 | #[glib::object_subclass] 25 | impl ObjectSubclass for ContainersListView { 26 | const NAME: &'static str = "PdsContainersListView"; 27 | type Type = super::ContainersListView; 28 | type ParentType = gtk::Widget; 29 | 30 | fn class_init(klass: &mut Self::Class) { 31 | klass.bind_template(); 32 | } 33 | 34 | fn instance_init(obj: &glib::subclass::InitializingObject) { 35 | obj.init_template(); 36 | } 37 | } 38 | 39 | impl ObjectImpl for ContainersListView { 40 | fn properties() -> &'static [glib::ParamSpec] { 41 | Self::derived_properties() 42 | } 43 | 44 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 45 | self.derived_set_property(id, value, pspec); 46 | } 47 | 48 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 49 | self.derived_property(id, pspec) 50 | } 51 | 52 | fn dispose(&self) { 53 | utils::unparent_children(&*self.obj()); 54 | } 55 | } 56 | 57 | impl WidgetImpl for ContainersListView {} 58 | 59 | impl ContainersListView { 60 | pub(super) fn set_model(&self, value: Option<&gio::ListModel>) { 61 | let obj = &*self.obj(); 62 | if obj.model().as_ref() == value { 63 | return; 64 | } 65 | 66 | self.list_box.bind_model(value, |item| { 67 | view::ContainerRow::from(item.downcast_ref().unwrap()).upcast() 68 | }); 69 | 70 | self.model.set(value); 71 | } 72 | } 73 | } 74 | 75 | glib::wrapper! { 76 | pub(crate) struct ContainersListView(ObjectSubclass) 77 | @extends gtk::Widget, 78 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 79 | } 80 | 81 | impl Default for ContainersListView { 82 | fn default() -> Self { 83 | glib::Object::builder().build() 84 | } 85 | } 86 | 87 | impl From> for ContainersListView { 88 | fn from(model: Option<&gio::ListModel>) -> Self { 89 | glib::Object::builder().property("model", model).build() 90 | } 91 | } 92 | 93 | impl ContainersListView { 94 | pub(crate) fn select_visible(&self) { 95 | (0..) 96 | .map(|pos| self.imp().list_box.row_at_index(pos)) 97 | .take_while(Option::is_some) 98 | .flatten() 99 | .for_each(|row| { 100 | row.downcast_ref::() 101 | .unwrap() 102 | .container() 103 | .unwrap() 104 | .set_selected(row.is_visible()); 105 | }); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/view/container_renamer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use adw::prelude::*; 4 | use adw::subclass::prelude::*; 5 | use glib::Properties; 6 | use glib::clone; 7 | use gtk::CompositeTemplate; 8 | use gtk::glib; 9 | 10 | use crate::model; 11 | use crate::utils; 12 | use crate::widget; 13 | 14 | mod imp { 15 | use super::*; 16 | 17 | #[derive(Debug, Default, Properties, CompositeTemplate)] 18 | #[properties(wrapper_type = super::ContainerRenamer)] 19 | #[template(resource = "/com/github/marhkb/Pods/ui/view/container_renamer.ui")] 20 | pub(crate) struct ContainerRenamer { 21 | #[property(get, set, construct_only, nullable)] 22 | pub(super) container: glib::WeakRef, 23 | #[property(get, set)] 24 | pub(super) new_name: RefCell, 25 | #[template_child] 26 | pub(super) entry_row: TemplateChild, 27 | } 28 | 29 | #[glib::object_subclass] 30 | impl ObjectSubclass for ContainerRenamer { 31 | const NAME: &'static str = "PdsContainerRenamer"; 32 | type Type = super::ContainerRenamer; 33 | type ParentType = gtk::Widget; 34 | 35 | fn class_init(klass: &mut Self::Class) { 36 | klass.bind_template(); 37 | } 38 | 39 | fn instance_init(obj: &glib::subclass::InitializingObject) { 40 | obj.init_template(); 41 | } 42 | } 43 | 44 | impl ObjectImpl for ContainerRenamer { 45 | fn properties() -> &'static [glib::ParamSpec] { 46 | Self::derived_properties() 47 | } 48 | 49 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 50 | self.derived_set_property(id, value, pspec); 51 | } 52 | 53 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 54 | self.derived_property(id, pspec) 55 | } 56 | 57 | fn constructed(&self) { 58 | self.parent_constructed(); 59 | 60 | let obj = &*self.obj(); 61 | 62 | if let Some(container) = obj.container() { 63 | self.entry_row.set_text(&container.name()); 64 | } 65 | 66 | self.entry_row 67 | .bind_property("text", obj, "new-name") 68 | .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) 69 | .build(); 70 | } 71 | 72 | fn dispose(&self) { 73 | utils::unparent_children(&*self.obj()); 74 | } 75 | } 76 | 77 | impl WidgetImpl for ContainerRenamer { 78 | fn root(&self) { 79 | self.parent_root(); 80 | 81 | let widget = &*self.obj(); 82 | 83 | glib::idle_add_local(clone!( 84 | #[weak] 85 | widget, 86 | #[upgrade_or] 87 | glib::ControlFlow::Break, 88 | move || { 89 | widget.imp().entry_row.grab_focus(); 90 | glib::ControlFlow::Break 91 | } 92 | )); 93 | } 94 | } 95 | } 96 | 97 | glib::wrapper! { 98 | pub(crate) struct ContainerRenamer(ObjectSubclass) 99 | @extends gtk::Widget, 100 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 101 | } 102 | 103 | impl From<&model::Container> for ContainerRenamer { 104 | fn from(container: &model::Container) -> Self { 105 | glib::Object::builder() 106 | .property("container", container) 107 | .build() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/view/volumes_prune_page.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::podman; 9 | use crate::utils; 10 | use crate::view; 11 | use crate::widget; 12 | 13 | const ACTION_PRUNE: &str = "volumes-prune-page.prune"; 14 | 15 | mod imp { 16 | use super::*; 17 | 18 | #[derive(Debug, Default, Properties, CompositeTemplate)] 19 | #[properties(wrapper_type = super::VolumesPrunePage)] 20 | #[template(resource = "/com/github/marhkb/Pods/ui/view/volumes_prune_page.ui")] 21 | pub(crate) struct VolumesPrunePage { 22 | #[property(get, set, construct_only, nullable)] 23 | pub(super) client: glib::WeakRef, 24 | #[template_child] 25 | pub(super) navigation_view: TemplateChild, 26 | #[template_child] 27 | pub(super) prune_until_row: TemplateChild, 28 | } 29 | 30 | #[glib::object_subclass] 31 | impl ObjectSubclass for VolumesPrunePage { 32 | const NAME: &'static str = "PdsVolumesPrunePage"; 33 | type Type = super::VolumesPrunePage; 34 | type ParentType = gtk::Widget; 35 | 36 | fn class_init(klass: &mut Self::Class) { 37 | klass.bind_template(); 38 | 39 | klass.install_action(ACTION_PRUNE, None, |widget, _, _| { 40 | widget.prune(); 41 | }); 42 | } 43 | 44 | fn instance_init(obj: &glib::subclass::InitializingObject) { 45 | obj.init_template(); 46 | } 47 | } 48 | 49 | impl ObjectImpl for VolumesPrunePage { 50 | fn properties() -> &'static [glib::ParamSpec] { 51 | Self::derived_properties() 52 | } 53 | 54 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 55 | self.derived_set_property(id, value, pspec); 56 | } 57 | 58 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 59 | self.derived_property(id, pspec) 60 | } 61 | 62 | fn dispose(&self) { 63 | utils::unparent_children(&*self.obj()); 64 | } 65 | } 66 | 67 | impl WidgetImpl for VolumesPrunePage {} 68 | } 69 | 70 | glib::wrapper! { 71 | pub(crate) struct VolumesPrunePage(ObjectSubclass) 72 | @extends gtk::Widget, 73 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 74 | } 75 | 76 | impl From<&model::Client> for VolumesPrunePage { 77 | fn from(client: &model::Client) -> Self { 78 | glib::Object::builder().property("client", client).build() 79 | } 80 | } 81 | 82 | impl VolumesPrunePage { 83 | pub(crate) fn prune(&self) { 84 | let imp = self.imp(); 85 | 86 | let action = self.client().unwrap().action_list().prune_volumes( 87 | podman::opts::VolumePruneOpts::builder() 88 | .filter(if imp.prune_until_row.enables_expansion() { 89 | Some(podman::opts::VolumePruneFilter::Until( 90 | imp.prune_until_row.prune_until_timestamp().to_string(), 91 | )) 92 | } else { 93 | None 94 | }) 95 | .build(), 96 | ); 97 | 98 | let page = view::ActionPage::from(&action); 99 | 100 | imp.navigation_view.push( 101 | &adw::NavigationPage::builder() 102 | .can_pop(false) 103 | .child(&page) 104 | .build(), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/view/containers_prune_page.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::*; 2 | use adw::subclass::prelude::*; 3 | use glib::Properties; 4 | use gtk::CompositeTemplate; 5 | use gtk::glib; 6 | 7 | use crate::model; 8 | use crate::podman; 9 | use crate::utils; 10 | use crate::view; 11 | use crate::widget; 12 | 13 | const ACTION_PRUNE: &str = "containers-prune-page.prune"; 14 | 15 | mod imp { 16 | use super::*; 17 | 18 | #[derive(Debug, Default, Properties, CompositeTemplate)] 19 | #[properties(wrapper_type = super::ContainersPrunePage)] 20 | #[template(resource = "/com/github/marhkb/Pods/ui/view/containers_prune_page.ui")] 21 | pub(crate) struct ContainersPrunePage { 22 | #[property(get, set, construct_only, nullable)] 23 | pub(super) client: glib::WeakRef, 24 | #[template_child] 25 | pub(super) navigation_view: TemplateChild, 26 | #[template_child] 27 | pub(super) prune_until_row: TemplateChild, 28 | } 29 | 30 | #[glib::object_subclass] 31 | impl ObjectSubclass for ContainersPrunePage { 32 | const NAME: &'static str = "PdsContainersPrunePage"; 33 | type Type = super::ContainersPrunePage; 34 | type ParentType = gtk::Widget; 35 | 36 | fn class_init(klass: &mut Self::Class) { 37 | klass.bind_template(); 38 | 39 | klass.install_action(ACTION_PRUNE, None, |widget, _, _| { 40 | widget.prune(); 41 | }); 42 | } 43 | 44 | fn instance_init(obj: &glib::subclass::InitializingObject) { 45 | obj.init_template(); 46 | } 47 | } 48 | 49 | impl ObjectImpl for ContainersPrunePage { 50 | fn properties() -> &'static [glib::ParamSpec] { 51 | Self::derived_properties() 52 | } 53 | 54 | fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 55 | self.derived_set_property(id, value, pspec); 56 | } 57 | 58 | fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value { 59 | self.derived_property(id, pspec) 60 | } 61 | 62 | fn dispose(&self) { 63 | utils::unparent_children(&*self.obj()); 64 | } 65 | } 66 | 67 | impl WidgetImpl for ContainersPrunePage {} 68 | } 69 | 70 | glib::wrapper! { 71 | pub(crate) struct ContainersPrunePage(ObjectSubclass) 72 | @extends gtk::Widget, 73 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; 74 | } 75 | 76 | impl From<&model::Client> for ContainersPrunePage { 77 | fn from(client: &model::Client) -> Self { 78 | glib::Object::builder().property("client", client).build() 79 | } 80 | } 81 | 82 | impl ContainersPrunePage { 83 | fn prune(&self) { 84 | let imp = self.imp(); 85 | 86 | let action = self.client().unwrap().action_list().prune_containers( 87 | podman::opts::ContainerPruneOpts::builder() 88 | .filter(if imp.prune_until_row.enables_expansion() { 89 | Some(podman::opts::ContainerPruneFilter::Until( 90 | imp.prune_until_row.prune_until_timestamp().to_string(), 91 | )) 92 | } else { 93 | None 94 | }) 95 | .build(), 96 | ); 97 | 98 | let page = view::ActionPage::from(&action); 99 | 100 | imp.navigation_view.push( 101 | &adw::NavigationPage::builder() 102 | .can_pop(false) 103 | .child(&page) 104 | .build(), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /data/resources/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | assets/welcome.svg 5 | 6 | icons/scalable/actions/about-symbolic.svg 7 | icons/scalable/actions/ambulance-symbolic.svg 8 | icons/scalable/actions/arrow1-right-symbolic.svg 9 | icons/scalable/actions/bell-outline-symbolic.svg 10 | icons/scalable/actions/build-configure-symbolic.svg 11 | icons/scalable/actions/city-symbolic.svg 12 | icons/scalable/actions/code-symbolic.svg 13 | icons/scalable/actions/cross-symbolic.svg 14 | icons/scalable/actions/eraser5-symbolic.svg 15 | icons/scalable/actions/funnel-symbolic.svg 16 | icons/scalable/actions/get-symbolic.svg 17 | icons/scalable/actions/memory-symbolic.svg 18 | icons/scalable/actions/merge-symbolic.svg 19 | icons/scalable/actions/pip-out-symbolic.svg 20 | icons/scalable/actions/pods-symbolic.svg 21 | icons/scalable/actions/processor-symbolic.svg 22 | icons/scalable/actions/put-symbolic.svg 23 | icons/scalable/actions/puzzle-piece-symbolic.svg 24 | icons/scalable/actions/regex-symbolic.svg 25 | icons/scalable/actions/skull-symbolic.svg 26 | icons/scalable/actions/stacked-plates-symbolic.svg 27 | icons/scalable/actions/success-small-symbolic.svg 28 | icons/scalable/actions/uppercase-symbolic.svg 29 | icons/scalable/actions/whole-word-symbolic.svg 30 | icons/scalable/status/error-symbolic.svg 31 | icons/scalable/status/heart-broken-symbolic.svg 32 | icons/scalable/status/heart-filled-symbolic.svg 33 | icons/scalable/status/local-connection-symbolic.svg 34 | icons/scalable/status/success-symbolic.svg 35 | icons/scalable/status/verified-checkmark-symbolic.svg 36 | 37 | style.css 38 | style-dark.css 39 | style-hc.css 40 | style-hc-dark.css 41 | 42 | 43 | --------------------------------------------------------------------------------