[
  {
    "path": ".github/workflows/ci.yml",
    "content": "on: [push, pull_request]\n\nname: Continuous integration\n\njobs:\n  check:\n    name: Check\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          override: true\n\n      - name: Install dependencies\n        run: |\n          sudo apt update\n          sudo apt install -y libgtk-4-dev\n\n      - name: Set up cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n\n      - name: Run cargo check\n        uses: actions-rs/cargo@v1\n        with:\n          command: check\n\n  test:\n    name: Test Suite\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          override: true\n\n      - name: Install dependencies\n        run: |\n          sudo apt update\n          sudo apt install -y libgtk-4-dev\n\n      - name: Set up cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n\n      - name: Run cargo test\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n\n  lints:\n    name: Lints\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          override: true\n          components: rustfmt, clippy\n\n      - name: Run cargo fmt\n        uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: --all -- --check\n\n      - name: Install dependencies\n        run: |\n          sudo apt update\n          sudo apt install -y libgtk-4-dev\n\n      - name: Set up cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n\n      - name: Run cargo clippy\n        uses: actions-rs/cargo@v1\n        with:\n          command: clippy\n          args: -- -D warnings\n\n  build:\n    name: Build\n    needs: [check, test]\n    runs-on: ubuntu-22.04\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install stable toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: stable\n          override: true\n\n      - name: Install dependencies\n        run: |\n          sudo apt update\n          sudo apt install -y libgtk-4-dev\n\n      - name: Set up cache\n        uses: actions/cache@v2\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            target\n          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}\n\n      - name: Run cargo build\n        uses: actions-rs/cargo@v1\n        with:\n          command: build\n          args: --release\n\n      - name: Upload Artifact\n        uses: actions/upload-artifact@v2\n        with:\n          name: image-roll\n          path: target/release/image-roll\n\n      - name: Create debian package\n        run: |\n          cargo install cargo-deb\n          cargo deb\n\n      - name: Upload debian Artifact\n        uses: actions/upload-artifact@v2\n        with:\n          name: image-roll-deb\n          path: target/debian/image-roll*.deb\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/build-dir\nsrc/resources/image-roll_ui.glade~\n/.flatpak-builder\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"image-roll\"\nversion = \"2.1.0\"\nlicense = \"MIT\"\ndescription = \"Image Roll is a simple and fast GTK image viewer with basic image manipulation tools.\"\nhomepage = \"https://github.com/weclaw1/image-roll\"\nrepository = \"https://github.com/weclaw1/image-roll\"\nreadme = \"README.md\"\nauthors = [\"Robert Węcławski <r.weclawski@gmail.com>\"]\nedition = \"2021\"\n\n[dependencies]\nlog = \"0.4.17\"\nenv_logger = \"0.9.0\"\nanyhow = \"1.0.58\"\nashpd = { version = \"0.3.2\", optional = true }\n\n[dev-dependencies]\nitertools = \"0.10.3\"\nrand = \"0.8.5\"\ninfer = \"0.9.0\"\n\n[dependencies.gtk]\npackage = \"gtk4\"\nversion = \"0.4.8\"\nfeatures = [\"v4_4\"]\n\n[features]\ndefault = [\"wallpaper\"]\nwallpaper = [\"ashpd\"]  # set image as wallpaper\n\n[package.metadata.deb]\nlicense-file = [\"LICENSE\", \"0\"]\nsection = \"graphics\"\ndepends = \"$auto\"\nmaintainer-scripts = \"debian\"\nassets = [\n    [\"target/release/image-roll\", \"usr/bin/\", \"755\"],\n    [\"README.md\", \"usr/share/doc/image-roll/README\", \"644\"],\n    [\"src/resources/com.github.weclaw1.ImageRoll.desktop\", \"usr/share/applications/\", \"644\"],\n    [\"src/resources/com.github.weclaw1.ImageRoll.svg\", \"usr/share/icons/hicolor/scalable/apps/\", \"644\"],\n    [\"src/resources/com.github.weclaw1.ImageRoll.Devel.svg\", \"usr/share/icons/hicolor/scalable/apps/\", \"644\"],\n    [\"src/resources/com.github.weclaw1.ImageRoll-symbolic.svg\", \"usr/share/icons/hicolor/scalable/apps/\", \"644\"],\n    [\"src/resources/com.github.weclaw1.ImageRoll.gschema.xml\", \"/usr/share/glib-2.0/schemas/\", \"644\"],\n]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Robert Węcławski\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "\n# Image Roll\n![Image Roll](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/com.github.weclaw1.ImageRoll.svg)\n\n**Image Roll** is a simple and fast GTK image viewer with basic image manipulation tools.\n\n## Features\n- Written in Rust\n- uses modern GTK 4\n- adaptive - can be used on desktop and mobile devices\n- crop image\n- rotate image\n- resize image\n- undo and redo image edits\n\n![Screenshot](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/screenshot.png)\n\n## Installation\n\n### Requirements\nIf you use AUR or Flatpak you may skip this section.\n\nFor this application you are required to have at least GTK 4.4.\n\n#### Ubuntu/Debian\n```\nsudo apt install libgtk-4-dev\n```\n#### Fedora/CentOS\n```\nsudo dnf install gtk4-devel glib2-devel\n```\n\n### Flatpak\nFlatpak is the recommended install method.\nIn order to install Image Roll using Flatpak run:\n```\nflatpak install flathub com.github.weclaw1.ImageRoll\n```\n\n### Alpine Linux\nAlpine Linux provides [image-roll](https://pkgs.alpinelinux.org/packages?name=image-roll) package.\n```\napk add image-roll\n```\n\n### AUR\nIf you run Arch Linux, you can use one of the AUR packages.\nThere are 3, `image-roll`, `image-roll-bin`, and `image-roll-git`.\nReplace `yay` with your AUR helper of choice.\n\n```\nyay -S image-roll\n```\n\n### Debian package\nOn the releases page can be found deb packages which can be used on Debian and its derivatives.\n\n### Precompiled binaries\nReady-to-go executables can be found on the releases page.\n\n### Cargo\nTo install Image Roll using cargo run the following command:\n```\ncargo install image-roll\n```\n"
  },
  {
    "path": "build.rs",
    "content": "use std::{env, process::Command};\n\nfn main() {\n    Command::new(\"glib-compile-resources\")\n        .args(&[\"src/resources/resources.xml\", \"--sourcedir=src/resources\"])\n        .status()\n        .unwrap();\n\n    let python_installed = Command::new(\"sh\")\n        .args(&[\"-c\", \"command -v python3\"])\n        .status()\n        .unwrap()\n        .success();\n    let pip_installed = Command::new(\"sh\")\n        .args(&[\"-c\", \"command -v pip3\"])\n        .status()\n        .unwrap()\n        .success();\n    let wget_installed = Command::new(\"sh\")\n        .args(&[\"-c\", \"command -v wget\"])\n        .status()\n        .unwrap()\n        .success();\n\n    if python_installed && pip_installed && wget_installed {\n        Command::new(\"pip3\")\n            .args(&[\"install\", \"aiohttp\", \"toml\"])\n            .status()\n            .unwrap();\n        Command::new(\"wget\").arg(\"https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py\").status().unwrap();\n        Command::new(\"python3\")\n            .args(&[\n                \"flatpak-cargo-generator.py\",\n                \"Cargo.lock\",\n                \"-o\",\n                \"src/resources/cargo-sources.json\",\n            ])\n            .status()\n            .unwrap();\n        Command::new(\"rm\")\n            .arg(\"flatpak-cargo-generator.py\")\n            .status()\n            .unwrap();\n    }\n\n    if Ok(\"debug\".to_owned()) == env::var(\"PROFILE\") {\n        Command::new(\"sh\")\n            .args(&[\"-c\", \"mkdir -p $HOME/.local/share/glib-2.0/schemas\"])\n            .status()\n            .unwrap();\n        Command::new(\"sh\").args(&[\"-c\", \"install -D src/resources/com.github.weclaw1.ImageRoll.gschema.xml $HOME/.local/share/glib-2.0/schemas/\"]).status().unwrap();\n        Command::new(\"sh\")\n            .args(&[\n                \"-c\",\n                \"glib-compile-schemas $HOME/.local/share/glib-2.0/schemas/\",\n            ])\n            .status()\n            .unwrap();\n    }\n\n    println!(\"cargo:rerun-if-changed=src/resources/resources.xml\");\n    println!(\"cargo:rerun-if-changed=src/resources/image-roll.ui\");\n    println!(\"cargo:rerun-if-changed=src/resources/icons/crop-symbolic.svg\");\n    println!(\"cargo:rerun-if-changed=src/resources/com.github.weclaw1.ImageRoll.svg\");\n    println!(\"cargo:rerun-if-changed=src/resources/com.github.weclaw1.ImageRoll.gschema.xml\");\n    println!(\"cargo:rerun-if-changed=Cargo.lock\");\n}\n"
  },
  {
    "path": "debian/postinst",
    "content": "#!/bin/sh\nset -e\nglib-compile-schemas /usr/share/glib-2.0/schemas\nexit 0"
  },
  {
    "path": "src/app.rs",
    "content": "use gtk::{\n    gdk::Display,\n    gio,\n    glib::{self, timeout_future},\n    prelude::*,\n    Builder,\n};\n\nuse std::{\n    cell::{Cell, RefCell},\n    rc::Rc,\n    time::Duration,\n};\n\nuse crate::image_list::ImageList;\nuse crate::settings::Settings;\nuse crate::ui::{\n    event::{post_event, Event},\n    widgets::Widgets,\n};\nuse crate::{file_list::FileList, ui::controllers::Controllers};\nuse crate::{\n    image::CoordinatesPair,\n    ui::{action, event},\n};\n\npub struct App {\n    application: gtk::Application,\n    controllers: Controllers,\n    widgets: Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    file_list: FileList,\n    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,\n    settings: Settings,\n    sender: glib::Sender<Event>,\n}\n\nimpl App {\n    pub fn create(application: &gtk::Application, file: Option<&gio::File>) {\n        let bytes = glib::Bytes::from_static(include_bytes!(\"resources/resources.gresource\"));\n        let resources = gio::Resource::from_data(&bytes).expect(\"Couldn't load resources\");\n        gio::resources_register(&resources);\n\n        let builder = Builder::from_resource(\"/com/github/weclaw1/image-roll/image-roll.ui\");\n\n        let widgets: Widgets = Widgets::init(builder, application);\n\n        let controllers = Controllers::init();\n\n        gtk::IconTheme::for_display(&Display::default().unwrap())\n            .add_resource_path(\"/com/github/weclaw1/image-roll/icons/\");\n\n        let image_list: Rc<RefCell<ImageList>> = Rc::new(RefCell::new(ImageList::new()));\n\n        let file_list: FileList = FileList::new(None).unwrap();\n\n        let selection_coords: Rc<Cell<Option<CoordinatesPair>>> = Rc::new(Cell::new(None));\n\n        let settings: Settings = Settings::new(application.application_id().unwrap().as_str());\n\n        let (window_width, window_height) = settings.window_size();\n        widgets\n            .window()\n            .set_default_size(window_width as i32, window_height as i32);\n\n        let (sender, receiver) = glib::MainContext::channel::<Event>(glib::PRIORITY_DEFAULT);\n\n        if let Some(file) = file {\n            let main_context = glib::MainContext::default();\n            let second_sender = sender.clone();\n            let file = file.clone();\n            main_context.spawn_local(async move {\n                timeout_future(Duration::from_millis(10)).await;\n                post_event(&second_sender, Event::OpenFile(file));\n            });\n        }\n\n        let mut app = Self {\n            application: application.clone(),\n            controllers,\n            widgets,\n            image_list,\n            file_list,\n            selection_coords,\n            settings,\n            sender,\n        };\n\n        event::connect_events(\n            app.widgets.clone(),\n            app.sender.clone(),\n            app.image_list.clone(),\n            app.selection_coords.clone(),\n            app.settings.clone(),\n        );\n\n        event::connect_controllers(\n            app.sender.clone(),\n            app.widgets.clone(),\n            app.controllers.clone(),\n        );\n\n        action::update_buttons_state(\n            &app.widgets,\n            &app.file_list,\n            app.image_list.clone(),\n            &app.settings,\n        );\n\n        receiver.attach(None, move |e| {\n            app.process_event(e);\n            glib::Continue(true)\n        });\n    }\n\n    pub fn process_event(&mut self, event: Event) {\n        match event {\n            Event::OpenFile(file) => action::open_file(\n                &self.sender,\n                self.image_list.clone(),\n                &mut self.file_list,\n                file,\n            ),\n            Event::LoadImage(file_path) => action::load_image(\n                &self.sender,\n                &mut self.settings,\n                &self.widgets,\n                self.image_list.clone(),\n                file_path,\n            ),\n            Event::DisplayMessage(message, message_type) => {\n                action::display_message(&self.widgets, message.as_str(), message_type)\n            }\n            Event::ImageViewportResize(viewport_size) => {\n                action::image_viewport_resize(&self.sender, &mut self.settings, viewport_size)\n            }\n            Event::RefreshPreview(preview_size) => {\n                action::refresh_preview(&self.widgets, self.image_list.clone(), preview_size)\n            }\n            Event::ChangePreviewSize(preview_size) => action::change_preview_size(\n                &self.sender,\n                &self.widgets,\n                &mut self.settings,\n                preview_size,\n            ),\n            Event::ImageEdit(image_operation) => action::image_edit(\n                &self.sender,\n                &self.settings,\n                self.image_list.clone(),\n                &self.file_list,\n                image_operation,\n            ),\n            Event::StartSelection(position) if self.widgets.crop_button().is_active() => {\n                action::start_selection(\n                    &self.widgets,\n                    self.image_list.clone(),\n                    self.selection_coords.clone(),\n                    position,\n                )\n            }\n            Event::DragSelection(position) if self.widgets.crop_button().is_active() => {\n                action::drag_selection(\n                    &self.widgets,\n                    self.image_list.clone(),\n                    self.selection_coords.clone(),\n                    position,\n                )\n            }\n            Event::SaveCurrentImage(filename) => {\n                action::save_current_image(&self.sender, self.image_list.clone(), filename);\n                if self.file_list.current_folder_monitor_mut().is_none() {\n                    action::refresh_file_list(&self.sender, &mut self.file_list);\n                }\n            }\n            Event::DeleteCurrentImage => {\n                action::delete_current_image(\n                    &self.sender,\n                    &mut self.file_list,\n                    self.image_list.clone(),\n                );\n                if self.file_list.current_folder_monitor_mut().is_none() {\n                    action::refresh_file_list(&self.sender, &mut self.file_list);\n                }\n            }\n            Event::EndSelection if self.widgets.crop_button().is_active() => action::end_selection(\n                &self.sender,\n                &self.widgets,\n                self.image_list.clone(),\n                self.selection_coords.clone(),\n            ),\n            Event::PreviewSmaller(value) => {\n                action::preview_smaller(&self.sender, &self.settings, value)\n            }\n            Event::PreviewLarger(value) => {\n                action::preview_larger(&self.sender, &self.settings, value)\n            }\n            Event::PreviewFitScreen => action::preview_fit_screen(&self.sender),\n            Event::NextImage => {\n                action::next_image(&self.sender, self.image_list.clone(), &mut self.file_list)\n            }\n            Event::PreviousImage => {\n                action::previous_image(&self.sender, self.image_list.clone(), &mut self.file_list)\n            }\n            Event::RefreshFileList => action::refresh_file_list(&self.sender, &mut self.file_list),\n            Event::ResizePopoverDisplayed => {\n                action::resize_popover_displayed(&self.widgets, self.image_list.clone())\n            }\n            Event::UpdateResizePopoverWidth => {\n                action::update_resize_popover_width(&self.widgets, self.image_list.clone())\n            }\n            Event::UpdateResizePopoverHeight => {\n                action::update_resize_popover_height(&self.widgets, self.image_list.clone())\n            }\n            Event::UndoOperation => {\n                action::undo_operation(&self.sender, &self.settings, self.image_list.clone())\n            }\n            Event::RedoOperation => {\n                action::redo_operation(&self.sender, &self.settings, self.image_list.clone())\n            }\n            Event::Print => action::print(&self.sender, &self.widgets, self.image_list.clone()),\n            Event::HideInfoPanel => action::hide_info_panel(&self.widgets),\n            Event::ToggleFullscreen => action::toggle_fullscreen(&self.widgets, &mut self.settings),\n            Event::SetAsWallpaper => action::set_as_wallpaper(&self.sender, &self.file_list),\n            Event::StartZoomGesture => action::start_zoom_gesture(&mut self.settings),\n            Event::ZoomGestureScaleChanged(zoom_scale) => {\n                action::change_scale_on_zoom_gesture(&self.sender, &self.settings, zoom_scale)\n            }\n            Event::CopyCurrentImage => action::copy_current_image(self.image_list.clone()),\n            Event::Quit => action::quit(&self.application),\n            event => debug!(\"Discarded unused event: {:?}\", event),\n        }\n        action::update_buttons_state(\n            &self.widgets,\n            &self.file_list,\n            self.image_list.clone(),\n            &self.settings,\n        );\n    }\n}\n"
  },
  {
    "path": "src/file_list.rs",
    "content": "use std::path::PathBuf;\n\nuse anyhow::{anyhow, Result};\nuse gtk::{\n    gio::{self, Cancellable, FileMonitorFlags, FileQueryInfoFlags, FileType},\n    prelude::FileExt,\n};\n\npub struct FileList {\n    file_list: Vec<gio::FileInfo>,\n    current_file: Option<(usize, gio::File)>,\n    current_folder: Option<gio::File>,\n    current_folder_monitor: Option<gio::FileMonitor>,\n}\n\nimpl FileList {\n    pub fn new(current_file: Option<gio::File>) -> Result<FileList> {\n        if let Some(current_file) = current_file {\n            let current_folder = current_file.parent().ok_or_else(|| {\n                anyhow!(\n                    \"Couldn't get parent folder for file: {}\",\n                    current_file.parse_name()\n                )\n            })?;\n            let mut file_list: Vec<gio::FileInfo> = FileList::enumerate_files(&current_folder)?;\n            file_list.sort_by_key(|file| {\n                file.name()\n                    .file_name()\n                    .unwrap()\n                    .to_str()\n                    .unwrap()\n                    .to_owned()\n            });\n            let current_file_index = file_list\n                .iter()\n                .position(|file| file.name() == current_file.basename().unwrap_or_default())\n                .ok_or_else(|| {\n                    anyhow!(\n                        \"Couldn't find {} in enumerated files\",\n                        current_file.parse_name()\n                    )\n                })?;\n            let folder_monitor = current_folder\n                .monitor_directory(FileMonitorFlags::NONE, <Option<&Cancellable>>::None)\n                .ok();\n\n            if folder_monitor.is_none() {\n                warn!(\n                    \"Couldn't get monitor for directory: {}\",\n                    current_folder.path().unwrap().to_str().unwrap()\n                );\n            }\n\n            Ok(FileList {\n                file_list,\n                current_file: Some((current_file_index, current_file)),\n                current_folder: Some(current_folder),\n                current_folder_monitor: folder_monitor,\n            })\n        } else {\n            Ok(FileList {\n                file_list: Vec::new(),\n                current_file: None,\n                current_folder: None,\n                current_folder_monitor: None,\n            })\n        }\n    }\n\n    pub fn refresh(&mut self) -> Result<()> {\n        if let Some(current_folder) = &self.current_folder {\n            if !current_folder.query_exists(<Option<&Cancellable>>::None) {\n                self.file_list = Vec::new();\n                self.current_file = None;\n                self.current_folder = None;\n                return Ok(());\n            }\n            self.file_list = FileList::enumerate_files(current_folder)?;\n            self.file_list.sort_by_key(|file| {\n                file.name()\n                    .file_name()\n                    .unwrap()\n                    .to_str()\n                    .unwrap()\n                    .to_owned()\n            });\n\n            match &self.current_file {\n                Some((_, current_file)) => {\n                    let file_index = self.file_list.iter().position(|file| {\n                        file.name() == current_file.basename().unwrap_or_default()\n                    });\n                    if let Some(file_index) = file_index {\n                        self.current_file = Some((file_index, self.current_file.take().unwrap().1));\n                    } else {\n                        self.next();\n                    }\n                }\n                None => self.next(),\n            }\n        }\n        Ok(())\n    }\n\n    pub fn next(&mut self) {\n        if let Some(current_folder) = &self.current_folder {\n            self.current_file = match self.current_file.take() {\n                Some((_, _)) if self.file_list.is_empty() => None,\n                Some((index, _)) if index + 1 < self.file_list.len() => Some((\n                    index + 1,\n                    current_folder.child(self.file_list[index + 1].name()),\n                )),\n                Some((index, _)) if index + 1 >= self.file_list.len() => {\n                    Some((0, current_folder.child(self.file_list[0].name())))\n                }\n                None if !self.file_list.is_empty() => {\n                    Some((0, current_folder.child(self.file_list[0].name())))\n                }\n                _ => None,\n            }\n        }\n    }\n\n    pub fn previous(&mut self) {\n        if let Some(current_folder) = &self.current_folder {\n            self.current_file = match self.current_file.take() {\n                Some((_, _)) if self.file_list.is_empty() => None,\n                Some((index, _)) if index as i64 > 0 => Some((\n                    index - 1,\n                    current_folder.child(self.file_list[index - 1].name()),\n                )),\n                Some((index, _)) if index as i64 - 1 < 0 => Some((\n                    self.file_list.len() - 1,\n                    current_folder.child(self.file_list[self.file_list.len() - 1].name()),\n                )),\n                None if !self.file_list.is_empty() => {\n                    Some((0, current_folder.child(self.file_list[0].name())))\n                }\n                _ => None,\n            }\n        }\n    }\n\n    // pub fn current_folder(&self) -> Option<&gio::File> {\n    //     self.current_folder.as_ref()\n    // }\n\n    pub fn current_file(&self) -> Option<&gio::File> {\n        self.current_file.as_ref().map(|(_, file)| file)\n    }\n\n    #[allow(dead_code)] // currently used only with feature \"wallpaper\"\n    pub fn current_file_uri(&self) -> Option<String> {\n        self.current_file\n            .as_ref()\n            .map(|(_, file)| file.uri().to_string())\n    }\n\n    pub fn current_file_path(&self) -> Option<PathBuf> {\n        self.current_file.as_ref().and_then(|(_, file)| file.path())\n    }\n\n    pub fn len(&self) -> usize {\n        self.file_list.len()\n    }\n\n    fn enumerate_files(folder: &gio::File) -> Result<Vec<gio::FileInfo>> {\n        Ok(folder\n            .enumerate_children(\n                \"standard::*\",\n                FileQueryInfoFlags::NONE,\n                <Option<&Cancellable>>::None,\n            )?\n            .into_iter()\n            .filter_map(|file| file.ok())\n            .filter(|file| file.file_type() == FileType::Regular)\n            .filter(|file| {\n                file.content_type()\n                    .filter(|content_type| content_type.to_string().starts_with(\"image\"))\n                    .is_some()\n            })\n            .collect())\n    }\n\n    pub fn current_folder_monitor_mut(&mut self) -> Option<&mut gio::FileMonitor> {\n        self.current_folder_monitor.as_mut()\n    }\n\n    pub fn delete_current_file(&mut self) -> Result<PathBuf> {\n        let deleted_file = self\n            .current_file()\n            .ok_or_else(|| {\n                anyhow!(\"Cannot delete current file because file list does not have a current file\")\n            })?\n            .to_owned();\n        let deleted_file_path = deleted_file\n            .path()\n            .ok_or_else(|| anyhow!(\"Deleted file does not have a valid path\"))?;\n        self.next();\n        deleted_file.trash(<Option<&Cancellable>>::None)?;\n        Ok(deleted_file_path)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use itertools::Itertools;\n\n    use rand::{distributions::Alphanumeric, Rng};\n\n    use crate::test_utils::TestResources;\n\n    use super::*;\n\n    const TEST_IMAGE: &[u8] = include_bytes!(\"resources/test/test_image.png\");\n\n    #[test]\n    fn file_list_contains_image_files() {\n        let mut test_resources = TestResources::new(\"test/file_list_contains_image_files\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n        test_resources.add_file(\"tes2.png\", TEST_IMAGE);\n\n        let file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test.png\"),\n        )))\n        .unwrap();\n\n        assert_eq!(2, file_list.len());\n    }\n\n    #[test]\n    fn file_list_does_not_contain_other_files() {\n        let mut test_resources = TestResources::new(\"test/file_list_does_not_contain_other_files\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n        test_resources.add_file(\"test2.png\", TEST_IMAGE);\n        test_resources.add_file(\"test.txt\", \"test\");\n\n        let file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test.png\"),\n        )))\n        .unwrap();\n\n        assert_eq!(2, file_list.len());\n    }\n\n    #[test]\n    fn file_list_contains_images_without_extension() {\n        let mut test_resources =\n            TestResources::new(\"test/file_list_contains_images_without_extension\");\n        test_resources.add_file(\"test\", TEST_IMAGE);\n        test_resources.add_file(\"test2\", TEST_IMAGE);\n\n        let file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test\"),\n        )))\n        .unwrap();\n\n        assert_eq!(2, file_list.len());\n    }\n\n    #[test]\n    fn file_list_does_not_contain_other_files_without_extension() {\n        let mut test_resources =\n            TestResources::new(\"test/file_list_does_not_contain_other_files_without_extension\");\n        test_resources.add_file(\"test\", TEST_IMAGE);\n        test_resources.add_file(\"test2\", TEST_IMAGE);\n        test_resources.add_file(\"test\", TEST_IMAGE);\n        test_resources.add_file(\"testtxt\", \"test\");\n\n        let file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test\"),\n        )))\n        .unwrap();\n\n        assert_eq!(2, file_list.len());\n    }\n\n    #[test]\n    fn file_list_is_in_alphabetical_order() {\n        let mut test_resources = TestResources::new(\"test/file_list_is_in_alphabetical_order\");\n\n        let mut random_file_names: Vec<String> = rand::thread_rng()\n            .sample_iter(Alphanumeric)\n            .map(char::from)\n            .chunks(10)\n            .into_iter()\n            .map(|chunk| chunk.collect::<String>())\n            .take(100)\n            .map(|name| format!(\"{}.{}\", name, \"png\"))\n            .collect();\n\n        random_file_names\n            .iter()\n            .for_each(|file_name| test_resources.add_file(file_name, TEST_IMAGE));\n\n        random_file_names.sort();\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(\n            test_resources\n                .file_folder()\n                .join(random_file_names.first().unwrap()),\n        )))\n        .unwrap();\n\n        assert_eq!(100, file_list.len());\n\n        for file_name in random_file_names.iter() {\n            assert_eq!(\n                file_name,\n                file_list\n                    .current_file()\n                    .unwrap()\n                    .basename()\n                    .unwrap()\n                    .to_str()\n                    .unwrap()\n            );\n            file_list.next();\n        }\n    }\n\n    #[test]\n    fn refresh_file_list_loads_new_images() {\n        let mut test_resources = TestResources::new(\"test/refresh_file_list_loads_new_images\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test.png\"),\n        )))\n        .unwrap();\n        assert_eq!(1, file_list.len());\n\n        test_resources.add_file(\"test2.png\", TEST_IMAGE);\n        file_list.refresh().unwrap();\n\n        assert_eq!(2, file_list.len());\n    }\n\n    #[test]\n    fn refresh_file_list_removes_deleted_images() {\n        let mut test_resources =\n            TestResources::new(\"test/refresh_file_list_removes_deleted_images\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n        test_resources.add_file(\"test2.png\", TEST_IMAGE);\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test.png\"),\n        )))\n        .unwrap();\n        assert_eq!(2, file_list.len());\n\n        test_resources.remove_file(\"test2.png\");\n        file_list.refresh().unwrap();\n\n        assert_eq!(1, file_list.len());\n    }\n\n    #[test]\n    fn test_change_to_next_image() {\n        let mut empty_file_list = FileList::new(None).unwrap();\n        assert!(empty_file_list.current_file().is_none());\n        empty_file_list.next();\n        assert!(empty_file_list.current_file().is_none());\n\n        let mut test_resources = TestResources::new(\"test/test_change_to_next_image\");\n        test_resources.add_file(\"test1.png\", TEST_IMAGE);\n        test_resources.add_file(\"test2.png\", TEST_IMAGE);\n        test_resources.add_file(\"test3.png\", TEST_IMAGE);\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test2.png\"),\n        )))\n        .unwrap();\n\n        file_list.next();\n        assert_eq!(\n            \"test3.png\",\n            file_list\n                .current_file()\n                .unwrap()\n                .basename()\n                .unwrap()\n                .to_str()\n                .unwrap()\n        );\n\n        file_list.next();\n        assert_eq!(\n            \"test1.png\",\n            file_list\n                .current_file()\n                .unwrap()\n                .basename()\n                .unwrap()\n                .to_str()\n                .unwrap()\n        );\n    }\n\n    #[test]\n    fn test_change_to_previous_image() {\n        let mut empty_file_list = FileList::new(None).unwrap();\n        assert!(empty_file_list.current_file().is_none());\n        empty_file_list.previous();\n        assert!(empty_file_list.current_file().is_none());\n\n        let mut test_resources = TestResources::new(\"test/test_change_to_previous_image\");\n        test_resources.add_file(\"test1.png\", TEST_IMAGE);\n        test_resources.add_file(\"test2.png\", TEST_IMAGE);\n        test_resources.add_file(\"test3.png\", TEST_IMAGE);\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(\n            test_resources.file_folder().join(\"test2.png\"),\n        )))\n        .unwrap();\n\n        file_list.previous();\n        assert_eq!(\n            \"test1.png\",\n            file_list\n                .current_file()\n                .unwrap()\n                .basename()\n                .unwrap()\n                .to_str()\n                .unwrap()\n        );\n\n        file_list.previous();\n        assert_eq!(\n            \"test3.png\",\n            file_list\n                .current_file()\n                .unwrap()\n                .basename()\n                .unwrap()\n                .to_str()\n                .unwrap()\n        );\n    }\n\n    #[test]\n    fn delete_current_file_deletes_file_from_filesystem() {\n        let mut test_resources =\n            TestResources::new(\"test/delete_current_file_deletes_file_from_filesystem\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap();\n\n        file_list.delete_current_file().unwrap();\n\n        assert!(std::fs::File::open(image_path).is_err());\n    }\n\n    #[test]\n    fn delete_current_file_returns_deleted_file_path() {\n        let mut test_resources =\n            TestResources::new(\"test/delete_current_file_returns_deleted_file_path\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap();\n\n        let file_list_current_file_path = file_list.current_file_path().unwrap();\n        let deleted_file_path = file_list.delete_current_file().unwrap();\n\n        assert_eq!(file_list_current_file_path, deleted_file_path);\n    }\n\n    #[test]\n    fn file_list_goes_to_next_file_after_removal_of_current_file() {\n        let mut test_resources =\n            TestResources::new(\"test/file_list_goes_to_next_file_after_removal_of_current_file\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n        test_resources.add_file(\"test2.png\", TEST_IMAGE);\n        test_resources.add_file(\"test3.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n\n        let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap();\n\n        let deleted_file_path = file_list.delete_current_file().unwrap();\n\n        assert_ne!(deleted_file_path, file_list.current_file_path().unwrap());\n    }\n}\n"
  },
  {
    "path": "src/image.rs",
    "content": "use std::path::Path;\n\nuse anyhow::{anyhow, Result};\nuse gtk::gdk_pixbuf::{InterpType, Pixbuf};\n\nuse crate::image_operation::{ApplyImageOperation, ImageOperation};\n\npub type Coordinates = (u32, u32);\npub type CoordinatesPair = (Coordinates, Coordinates);\n\npub struct Image {\n    original_image_buffer: Option<Pixbuf>,\n    current_image_buffer: Option<Pixbuf>,\n    preview_image_buffer: Option<Pixbuf>,\n    operations: Vec<ImageOperation>,\n    current_operation_index: Option<usize>,\n}\n\nimpl Image {\n    pub fn load<P: AsRef<Path>>(path: P) -> Result<Image> {\n        let image_buffer = Pixbuf::from_file(path)?;\n        Ok(Image {\n            original_image_buffer: Some(image_buffer.clone()),\n            current_image_buffer: Some(image_buffer),\n            preview_image_buffer: None,\n            operations: Vec::new(),\n            current_operation_index: None,\n        })\n    }\n\n    pub fn save<P: AsRef<Path>>(&mut self, path: P, clear_operations: bool) -> Result<()> {\n        let current_image_buffer = self\n            .current_image_buffer\n            .as_mut()\n            .ok_or_else(|| anyhow!(\"Image buffer is missing!\"))?;\n        let extension = path\n            .as_ref()\n            .extension()\n            .and_then(|extension| extension.to_str())\n            .ok_or_else(|| anyhow!(\"File path doesn't have file extension\"))?;\n        let lowercase_extension = extension.to_lowercase();\n        let file_type = match lowercase_extension.as_str() {\n            file_type @ \"jpeg\"\n            | file_type @ \"png\"\n            | file_type @ \"tiff\"\n            | file_type @ \"ico\"\n            | file_type @ \"bmp\" => file_type,\n            \"jpg\" => \"jpeg\",\n            \"tif\" => \"tiff\",\n            _ => \"png\",\n        };\n\n        let options: &[(&str, &str)] = match file_type {\n            \"jpeg\" => &[(\"quality\", \"100\")],\n            \"png\" => &[(\"compression\", \"9\")],\n            _ => &[],\n        };\n        current_image_buffer.savev(path.as_ref(), file_type, options)?;\n        if clear_operations {\n            self.original_image_buffer = Some(current_image_buffer.clone());\n            self.current_operation_index = None;\n            self.operations.clear();\n        }\n\n        Ok(())\n    }\n\n    pub fn reload<P: AsRef<Path>>(self, path: P) -> Result<Image> {\n        let original_image_buffer = Pixbuf::from_file(path)?;\n        let mut current_image_buffer = original_image_buffer.clone();\n        if let Some(current_operation_index) = self.current_operation_index {\n            current_image_buffer = self\n                .operations\n                .iter()\n                .take(current_operation_index + 1)\n                .fold(current_image_buffer, |image, operation| {\n                    image.apply_operation(operation).unwrap_or(image)\n                });\n        }\n        Ok(Image {\n            original_image_buffer: Some(original_image_buffer),\n            current_image_buffer: Some(current_image_buffer),\n            preview_image_buffer: None,\n            operations: self.operations,\n            current_operation_index: self.current_operation_index,\n        })\n    }\n\n    pub fn remove_image_buffers(&mut self) {\n        self.original_image_buffer = None;\n        self.current_image_buffer = None;\n        self.preview_image_buffer = None;\n    }\n\n    fn image_buffer_scale_to_fit(&self, canvas_width: u32, canvas_height: u32) -> Option<Pixbuf> {\n        if let Some(image_buffer) = &self.current_image_buffer {\n            let image_width = image_buffer.width() as f64;\n            let image_height = image_buffer.height() as f64;\n            let width_ratio = canvas_width as f64 / image_width;\n            let height_ratio = canvas_height as f64 / image_height;\n            let scale_ratio = width_ratio.min(height_ratio);\n            image_buffer.scale_simple(\n                (image_width * scale_ratio) as i32,\n                (image_height * scale_ratio) as i32,\n                InterpType::Nearest,\n            )\n        } else {\n            None\n        }\n    }\n\n    fn image_buffer_resize(&self, scale: u32) -> Option<Pixbuf> {\n        if let Some(image_buffer) = &self.current_image_buffer {\n            image_buffer.scale_simple(\n                (image_buffer.width() as f64 * (scale as f64 / 100.0)) as i32,\n                (image_buffer.height() as f64 * (scale as f64 / 100.0)) as i32,\n                InterpType::Bilinear,\n            )\n        } else {\n            None\n        }\n    }\n\n    pub fn create_preview_image_buffer(&mut self, preview_size: PreviewSize) {\n        self.preview_image_buffer = match preview_size {\n            PreviewSize::BestFit(canvas_width, canvas_height) => {\n                self.image_buffer_scale_to_fit(canvas_width, canvas_height)\n            }\n            PreviewSize::OriginalSize => self.current_image_buffer.clone(),\n            PreviewSize::Resized(scale) => self.image_buffer_resize(scale),\n        };\n    }\n\n    pub fn create_print_image_buffer(\n        &self,\n        canvas_width: u32,\n        canvas_height: u32,\n    ) -> Option<Pixbuf> {\n        if let Some((image_width, image_height)) = self.image_size() {\n            if image_width > canvas_width || image_height > canvas_height {\n                self.image_buffer_scale_to_fit(canvas_width, canvas_height)\n            } else {\n                self.current_image_buffer.clone()\n            }\n        } else {\n            None\n        }\n    }\n\n    pub fn preview_image_buffer(&self) -> Option<&Pixbuf> {\n        self.preview_image_buffer.as_ref()\n    }\n\n    pub fn current_image_buffer(&self) -> Option<&Pixbuf> {\n        self.current_image_buffer.as_ref()\n    }\n\n    pub fn image_size(&self) -> Option<(u32, u32)> {\n        self.current_image_buffer\n            .as_ref()\n            .map(|image_buffer| (image_buffer.width() as u32, image_buffer.height() as u32))\n    }\n\n    pub fn image_aspect_ratio(&self) -> Option<f64> {\n        self.image_size()\n            .map(|(image_width, image_height)| image_width as f64 / image_height as f64)\n    }\n\n    pub fn preview_image_buffer_size(&self) -> Option<(u32, u32)> {\n        self.preview_image_buffer\n            .as_ref()\n            .map(|image_buffer| (image_buffer.width() as u32, image_buffer.height() as u32))\n    }\n\n    pub fn preview_coords_to_image_coords(\n        &self,\n        coords: CoordinatesPair,\n    ) -> Option<CoordinatesPair> {\n        let ((start_coord_x, start_coord_y), (end_coord_x, end_coord_y)) = coords;\n        if let Some((image_width, image_height)) = self.image_size() {\n            if let Some((preview_width, preview_height)) = self.preview_image_buffer_size() {\n                Some((\n                    (\n                        (start_coord_x as f64 * (image_width as f64 / preview_width as f64)) as u32,\n                        (start_coord_y as f64 * (image_height as f64 / preview_height as f64))\n                            as u32,\n                    ),\n                    (\n                        (end_coord_x as f64 * (image_width as f64 / preview_width as f64)) as u32,\n                        (end_coord_y as f64 * (image_height as f64 / preview_height as f64)) as u32,\n                    ),\n                ))\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    }\n\n    pub fn has_operations(&self) -> bool {\n        !self.operations.is_empty() && self.current_operation_index.is_some()\n    }\n\n    pub fn can_undo_operation(&self) -> bool {\n        self.current_operation_index.is_some()\n    }\n\n    pub fn undo_operation(&mut self) {\n        if self.can_undo_operation() {\n            self.current_operation_index = self.current_operation_index.unwrap().checked_sub(1);\n            self.current_image_buffer = Some(\n                self.operations\n                    .iter()\n                    .take(\n                        self.current_operation_index\n                            .map_or(0, |operation_index| operation_index + 1),\n                    )\n                    .fold(\n                        self.original_image_buffer.clone().unwrap(),\n                        |image, operation| image.apply_operation(operation).unwrap_or(image),\n                    ),\n            );\n        }\n    }\n\n    pub fn can_redo_operation(&self) -> bool {\n        match self.current_operation_index {\n            Some(operation_index) => operation_index + 1 < self.operations.len(),\n            None => !self.operations.is_empty(),\n        }\n    }\n\n    pub fn redo_operation(&mut self) {\n        if self.can_redo_operation() {\n            self.current_operation_index = self\n                .current_operation_index\n                .map_or(Some(0), |current_operation_index| {\n                    Some(current_operation_index + 1)\n                });\n            self.current_image_buffer = Some(\n                self.operations\n                    .iter()\n                    .take(self.current_operation_index.unwrap() + 1)\n                    .fold(\n                        self.original_image_buffer.clone().unwrap(),\n                        |image, operation| image.apply_operation(operation).unwrap_or(image),\n                    ),\n            );\n        }\n    }\n}\n\nimpl ApplyImageOperation for Image {\n    type Result = Self;\n\n    fn apply_operation(mut self, image_operation: &ImageOperation) -> Self::Result {\n        if let Some(image_buffer) = self.current_image_buffer {\n            let applied_operation_image_buffer = image_buffer.apply_operation(image_operation);\n            if applied_operation_image_buffer.is_some() {\n                if let Some(current_operation_index) = self.current_operation_index {\n                    self.operations.truncate(current_operation_index + 1);\n                }\n                self.operations.push(*image_operation);\n                self.current_operation_index = Some(self.operations.len() - 1);\n            }\n            self.current_image_buffer =\n                Some(applied_operation_image_buffer.unwrap_or(image_buffer));\n        }\n        self\n    }\n}\n\n#[derive(Clone, Copy, Debug)]\npub enum PreviewSize {\n    BestFit(u32, u32),\n    OriginalSize,\n    Resized(u32),\n}\n\nimpl From<PreviewSize> for String {\n    fn from(value: PreviewSize) -> Self {\n        match value {\n            PreviewSize::BestFit(_, _) => String::from(\"Fit screen\"),\n            PreviewSize::OriginalSize => String::from(\"100%\"),\n            PreviewSize::Resized(value) => format!(\"{}%\", value),\n        }\n    }\n}\n\nimpl PreviewSize {\n    pub fn smaller(self) -> Option<PreviewSize> {\n        match self {\n            PreviewSize::BestFit(_, _) => Some(PreviewSize::OriginalSize),\n            PreviewSize::OriginalSize => Some(PreviewSize::Resized(75)),\n            PreviewSize::Resized(value) if value > 200 => Some(PreviewSize::Resized(200)),\n            PreviewSize::Resized(value) if value > 150 => Some(PreviewSize::Resized(150)),\n            PreviewSize::Resized(value) if value > 133 => Some(PreviewSize::Resized(133)),\n            PreviewSize::Resized(value) if value > 100 => Some(PreviewSize::OriginalSize),\n            PreviewSize::Resized(value) if value > 75 => Some(PreviewSize::Resized(75)),\n            PreviewSize::Resized(value) if value > 66 => Some(PreviewSize::Resized(66)),\n            PreviewSize::Resized(value) if value > 50 => Some(PreviewSize::Resized(50)),\n            PreviewSize::Resized(value) if value > 33 => Some(PreviewSize::Resized(33)),\n            PreviewSize::Resized(value) if value > 25 => Some(PreviewSize::Resized(25)),\n            PreviewSize::Resized(value) if value > 10 => Some(PreviewSize::Resized(10)),\n            PreviewSize::Resized(value) if value > 5 => Some(PreviewSize::Resized(5)),\n            PreviewSize::Resized(_) => None,\n        }\n    }\n\n    pub fn smaller_by(self, value: u32) -> Option<PreviewSize> {\n        let old_value = match self {\n            PreviewSize::BestFit(_, _) => return Some(PreviewSize::OriginalSize),\n            PreviewSize::OriginalSize => 100,\n            PreviewSize::Resized(value) => value,\n        };\n\n        old_value\n            .checked_sub(value)\n            .filter(|value| value >= &5)\n            .map(|value| {\n                if value == 100 {\n                    PreviewSize::OriginalSize\n                } else {\n                    PreviewSize::Resized(value)\n                }\n            })\n    }\n\n    pub fn can_be_smaller(&self) -> bool {\n        !matches!(self, PreviewSize::Resized(value) if value <= &5)\n    }\n\n    pub fn larger(self) -> Option<PreviewSize> {\n        match self {\n            PreviewSize::BestFit(_, _) => Some(PreviewSize::OriginalSize),\n            PreviewSize::OriginalSize => Some(PreviewSize::Resized(133)),\n            PreviewSize::Resized(value) if value < 10 => Some(PreviewSize::Resized(10)),\n            PreviewSize::Resized(value) if value < 25 => Some(PreviewSize::Resized(25)),\n            PreviewSize::Resized(value) if value < 33 => Some(PreviewSize::Resized(33)),\n            PreviewSize::Resized(value) if value < 50 => Some(PreviewSize::Resized(50)),\n            PreviewSize::Resized(value) if value < 66 => Some(PreviewSize::Resized(66)),\n            PreviewSize::Resized(value) if value < 75 => Some(PreviewSize::Resized(75)),\n            PreviewSize::Resized(value) if value < 100 => Some(PreviewSize::OriginalSize),\n            PreviewSize::Resized(value) if value < 133 => Some(PreviewSize::Resized(133)),\n            PreviewSize::Resized(value) if value < 150 => Some(PreviewSize::Resized(150)),\n            PreviewSize::Resized(value) if value < 200 => Some(PreviewSize::Resized(200)),\n            PreviewSize::Resized(value) if value < 500 => Some(PreviewSize::Resized(500)),\n            PreviewSize::Resized(_) => None,\n        }\n    }\n\n    pub fn larger_by(self, value: u32) -> Option<PreviewSize> {\n        let old_value = match self {\n            PreviewSize::BestFit(_, _) => return Some(PreviewSize::OriginalSize),\n            PreviewSize::OriginalSize => 100,\n            PreviewSize::Resized(value) => value,\n        };\n\n        old_value\n            .checked_add(value)\n            .filter(|value| value <= &500)\n            .map(|value| {\n                if value == 100 {\n                    PreviewSize::OriginalSize\n                } else {\n                    PreviewSize::Resized(value)\n                }\n            })\n    }\n\n    pub fn can_be_larger(&self) -> bool {\n        !matches!(self, PreviewSize::Resized(value) if value >= &500)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use gtk::gdk_pixbuf::PixbufRotation;\n\n    use crate::test_utils::TestResources;\n\n    use super::*;\n\n    const TEST_IMAGE: &[u8] = include_bytes!(\"resources/test/test_image.png\");\n\n    #[test]\n    fn test_load_image() {\n        let mut test_resources = TestResources::new(\"test/test_load_image\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        assert_eq!(\n            Pixbuf::from_file(test_resources.file_folder().join(\"test.png\"))\n                .unwrap()\n                .pixel_bytes(),\n            image.original_image_buffer.unwrap().pixel_bytes()\n        );\n        assert_eq!(\n            Pixbuf::from_file(test_resources.file_folder().join(\"test.png\"))\n                .unwrap()\n                .pixel_bytes(),\n            image.current_image_buffer.unwrap().pixel_bytes()\n        );\n        assert!(image.operations.is_empty());\n    }\n\n    #[test]\n    fn save_image() {\n        let mut test_resources = TestResources::new(\"test/save_image\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        let saved_file_path = test_resources.file_folder().join(\"test2.png\");\n        image.save(&saved_file_path, false).unwrap();\n        assert!(std::fs::File::open(saved_file_path).is_ok());\n    }\n\n    #[test]\n    fn test_save_image_without_clear_operations() {\n        let mut test_resources =\n            TestResources::new(\"test/test_save_image_without_clear_operations\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));\n        assert!(image.has_operations());\n        image\n            .save(test_resources.file_folder().join(\"test2.png\"), false)\n            .unwrap();\n        assert!(image.has_operations());\n        assert_ne!(\n            image.original_image_buffer.unwrap().pixel_bytes(),\n            image.current_image_buffer.unwrap().pixel_bytes()\n        )\n    }\n\n    #[test]\n    fn test_save_image_with_clear_operations() {\n        let mut test_resources = TestResources::new(\"test/test_save_image_with_clear_operations\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));\n        assert!(image.has_operations());\n        image\n            .save(test_resources.file_folder().join(\"test2.png\"), true)\n            .unwrap();\n        assert!(!image.has_operations());\n        assert_eq!(\n            image.original_image_buffer.unwrap().pixel_bytes(),\n            image.current_image_buffer.unwrap().pixel_bytes()\n        )\n    }\n\n    #[test]\n    fn save_image_uses_extensions_for_file_types_supported_by_pixbuf_save() {\n        let mut test_resources = TestResources::new(\n            \"test/save_image_uses_extensions_for_file_types_supported_by_pixbuf_save\",\n        );\n        let file_extensions = vec![\"png\", \"jpg\", \"tif\", \"ico\", \"bmp\"];\n        for extension in file_extensions {\n            let file_name = format!(\"{}.{}\", \"test\", extension);\n            test_resources.add_file(&file_name, TEST_IMAGE);\n            let mut image = Image::load(test_resources.file_folder().join(file_name)).unwrap();\n            let saved_file_path = test_resources\n                .file_folder()\n                .join(format!(\"{}.{}\", \"test2\", extension));\n            image.save(&saved_file_path, false).unwrap();\n            let saved_file_inferred_extension = infer::get_from_path(saved_file_path)\n                .unwrap()\n                .unwrap()\n                .extension();\n\n            assert_eq!(saved_file_inferred_extension, extension);\n        }\n    }\n\n    #[test]\n    fn file_extensions_jpg_and_jpeg_are_supported() {\n        let mut test_resources =\n            TestResources::new(\"test/save_file_extensions_jpg_and_jpeg_are_supported\");\n        test_resources.add_file(\"test.jpg\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.jpg\")).unwrap();\n        let saved_file_path = test_resources.file_folder().join(\"test2.jpg\");\n        image.save(&saved_file_path, false).unwrap();\n        let saved_file_inferred_extension = infer::get_from_path(saved_file_path)\n            .unwrap()\n            .unwrap()\n            .extension();\n        assert_eq!(saved_file_inferred_extension, \"jpg\");\n\n        test_resources.add_file(\"test.jpeg\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.jpeg\")).unwrap();\n        let saved_file_path = test_resources.file_folder().join(\"test2.jpeg\");\n        image.save(&saved_file_path, false).unwrap();\n        let saved_file_inferred_extension = infer::get_from_path(saved_file_path)\n            .unwrap()\n            .unwrap()\n            .extension();\n        assert_eq!(saved_file_inferred_extension, \"jpg\");\n    }\n\n    #[test]\n    fn test_image_reload() {\n        let mut test_resources = TestResources::new(\"test/test_image_reload\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));\n        let original_image_buffer = image.original_image_buffer.clone();\n        let current_image_buffer = image.current_image_buffer.clone();\n        image.remove_image_buffers();\n        assert!(image.original_image_buffer.is_none() && image.current_image_buffer.is_none());\n\n        image = image\n            .reload(test_resources.file_folder().join(\"test.png\"))\n            .unwrap();\n        assert_eq!(\n            original_image_buffer.unwrap().pixel_bytes(),\n            image.original_image_buffer.unwrap().pixel_bytes()\n        );\n        assert_eq!(\n            current_image_buffer.unwrap().pixel_bytes(),\n            image.current_image_buffer.unwrap().pixel_bytes()\n        );\n    }\n\n    #[test]\n    fn create_preview_original_size() {\n        let mut test_resources = TestResources::new(\"test/create_preview_original_size\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image.create_preview_image_buffer(PreviewSize::OriginalSize);\n\n        assert_eq!(\n            image.current_image_buffer.unwrap().pixel_bytes(),\n            image.preview_image_buffer.unwrap().pixel_bytes()\n        );\n    }\n\n    #[test]\n    fn create_preview_scale_to_fit() {\n        let mut test_resources = TestResources::new(\"test/create_preview_scale_to_fit\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((1000, 500)));\n        image.create_preview_image_buffer(PreviewSize::BestFit(500, 500));\n\n        assert_eq!((500, 250), image.preview_image_buffer_size().unwrap());\n    }\n\n    #[test]\n    fn create_preview_resized() {\n        let mut test_resources = TestResources::new(\"test/create_preview_resized\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((100, 100)));\n        image.create_preview_image_buffer(PreviewSize::Resized(90));\n\n        assert_eq!((90, 90), image.preview_image_buffer_size().unwrap());\n    }\n\n    #[test]\n    fn preview_coords_to_image_coords() {\n        let mut test_resources = TestResources::new(\"test/preview_coords_to_image_coords\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((100, 100)));\n        image.create_preview_image_buffer(PreviewSize::Resized(200));\n\n        assert_eq!(\n            ((10, 10), (20, 20)),\n            image\n                .preview_coords_to_image_coords(((20, 20), (40, 40)))\n                .unwrap()\n        );\n    }\n\n    #[test]\n    fn undo_operation() {\n        let mut test_resources = TestResources::new(\"test/undo_operation\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((100, 100)));\n        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));\n\n        assert!(image.can_undo_operation());\n        image.undo_operation();\n        assert!(\n            image.can_redo_operation()\n                && image.current_operation_index == Some(0)\n                && image.operations.len() == 2\n        );\n        image.undo_operation();\n        assert!(\n            image.can_redo_operation()\n                && image.current_operation_index == None\n                && image.operations.len() == 2\n        );\n    }\n\n    #[test]\n    fn redo_operation() {\n        let mut test_resources = TestResources::new(\"test/redo_operation\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((100, 100)));\n        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));\n\n        assert!(!image.can_redo_operation());\n        image.undo_operation();\n        assert!(\n            image.can_redo_operation()\n                && image.current_operation_index == Some(0)\n                && image.operations.len() == 2\n        );\n        image.redo_operation();\n        assert!(\n            !image.can_redo_operation()\n                && image.current_operation_index == Some(1)\n                && image.operations.len() == 2\n        );\n    }\n\n    #[test]\n    fn apply_operation() {\n        let mut test_resources = TestResources::new(\"test/apply_operation\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let mut image = Image::load(test_resources.file_folder().join(\"test.png\")).unwrap();\n\n        assert!(image.operations.is_empty() && image.current_operation_index.is_none());\n        image = image.apply_operation(&ImageOperation::Resize((100, 100)));\n        assert!(image.operations.len() == 1 && image.current_operation_index == Some(0));\n        assert!(\n            image.original_image_buffer.unwrap().pixel_bytes()\n                != image.current_image_buffer.unwrap().pixel_bytes()\n        );\n    }\n}\n"
  },
  {
    "path": "src/image_list.rs",
    "content": "use std::{\n    collections::HashMap,\n    ops::{Index, IndexMut},\n    path::{Path, PathBuf},\n};\n\nuse crate::image::Image;\n\nuse anyhow::{anyhow, Result};\nuse gtk::gdk::Texture;\n\npub struct ImageList {\n    images: HashMap<PathBuf, Image>,\n    current_image_path: Option<PathBuf>,\n}\n\nimpl ImageList {\n    pub fn new() -> Self {\n        Self {\n            images: HashMap::new(),\n            current_image_path: None,\n        }\n    }\n\n    pub fn remove(&mut self, key: &Path) -> Option<Image> {\n        self.images.remove(key)\n    }\n\n    pub fn insert(&mut self, key: PathBuf, value: Image) {\n        self.images.insert(key, value);\n    }\n\n    pub fn set_current_image_path(&mut self, current_image_path: Option<PathBuf>) {\n        self.current_image_path = current_image_path;\n    }\n\n    // pub fn current_image(&self) -> Option<&Image> {\n    //     self.current_image_path.as_ref().map(|image_path| self.images.get(image_path)).flatten()\n    // }\n\n    pub fn remove_current_image(&mut self) -> Option<Image> {\n        self.current_image_path\n            .clone()\n            .and_then(|image_path| self.remove(&image_path))\n    }\n\n    pub fn current_image_mut(&mut self) -> Option<&mut Image> {\n        self.current_image_path\n            .clone()\n            .and_then(move |image_path| self.images.get_mut(&image_path))\n    }\n\n    pub fn current_image(&self) -> Option<&Image> {\n        self.current_image_path\n            .as_ref()\n            .and_then(|image_path| self.images.get(image_path))\n    }\n\n    pub fn current_image_path(&self) -> Option<PathBuf> {\n        self.current_image_path.clone()\n    }\n\n    pub fn save_current_image(&mut self, filename: Option<PathBuf>) -> Result<()> {\n        let (filename, clear_operations) = if let Some(filename) = filename {\n            (filename, false)\n        } else {\n            (\n                self.current_image_path\n                    .clone()\n                    .ok_or_else(|| anyhow!(\"Current image path is not set\"))?,\n                true,\n            )\n        };\n\n        let current_image = self\n            .current_image_mut()\n            .ok_or_else(|| anyhow!(\"Couldn't load current image\"))?;\n\n        current_image.save(filename, clear_operations)?;\n        Ok(())\n    }\n\n    pub fn copy_current_image(&self, clipboard: gtk::gdk::Clipboard) {\n        if let Some(current_image) = self.current_image() {\n            if let Some(buffer) = current_image.current_image_buffer() {\n                clipboard.set_texture(&Texture::for_pixbuf(buffer));\n            }\n        }\n    }\n}\n\nimpl Index<&PathBuf> for ImageList {\n    type Output = Image;\n\n    fn index(&self, index: &PathBuf) -> &Self::Output {\n        &self.images[index]\n    }\n}\n\nimpl IndexMut<&PathBuf> for ImageList {\n    fn index_mut(&mut self, index: &PathBuf) -> &mut Self::Output {\n        self.images.get_mut(index).unwrap()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::{\n        image_operation::{ApplyImageOperation, ImageOperation},\n        test_utils::TestResources,\n    };\n\n    use super::*;\n\n    const TEST_IMAGE: &[u8] = include_bytes!(\"resources/test/test_image.png\");\n\n    #[test]\n    fn save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none() {\n        let mut test_resources = TestResources::new(\"test/save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n\n        let creation_date = std::fs::File::open(&image_path)\n            .unwrap()\n            .metadata()\n            .unwrap()\n            .modified()\n            .unwrap();\n\n        let image = Image::load(&image_path).unwrap();\n\n        let mut image_list = ImageList::new();\n        image_list.insert(image_path.clone(), image);\n        image_list.set_current_image_path(Some(image_path.clone()));\n        image_list.save_current_image(None).unwrap();\n\n        let modification_date = std::fs::File::open(&image_path)\n            .unwrap()\n            .metadata()\n            .unwrap()\n            .modified()\n            .unwrap();\n        assert!(modification_date > creation_date);\n    }\n\n    #[test]\n    fn save_current_image_creates_a_new_image_when_filename_is_set() {\n        let mut test_resources =\n            TestResources::new(\"test/save_current_image_creates_a_new_image_when_filename_is_set\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n        let image = Image::load(&image_path).unwrap();\n\n        let mut image_list = ImageList::new();\n        image_list.insert(image_path.clone(), image);\n        image_list.set_current_image_path(Some(image_path.clone()));\n\n        let new_image_path = test_resources.file_folder().join(\"test2.png\");\n        image_list\n            .save_current_image(Some(new_image_path.clone()))\n            .unwrap();\n\n        assert!(std::fs::File::open(new_image_path).is_ok());\n    }\n\n    #[test]\n    fn save_current_image_clears_image_operations_when_filename_is_set_to_none() {\n        let mut test_resources = TestResources::new(\n            \"test/save_current_image_clears_image_operations_when_filename_is_set_to_none\",\n        );\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n\n        let mut image = Image::load(&image_path).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((10, 10)));\n\n        let mut image_list = ImageList::new();\n        image_list.insert(image_path.clone(), image);\n        image_list.set_current_image_path(Some(image_path.clone()));\n\n        assert!(image_list.current_image().unwrap().has_operations());\n\n        image_list.save_current_image(None).unwrap();\n\n        assert!(!image_list.current_image().unwrap().has_operations());\n    }\n\n    #[test]\n    fn save_current_image_does_not_clear_image_operations_when_filename_is_set() {\n        let mut test_resources = TestResources::new(\n            \"test/save_current_image_does_not_clear_image_operations_when_filename_is_set\",\n        );\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let image_path = test_resources.file_folder().join(\"test.png\");\n\n        let mut image = Image::load(&image_path).unwrap();\n        image = image.apply_operation(&ImageOperation::Resize((10, 10)));\n\n        let mut image_list = ImageList::new();\n        image_list.insert(image_path.clone(), image);\n        image_list.set_current_image_path(Some(image_path.clone()));\n\n        assert!(image_list.current_image().unwrap().has_operations());\n\n        image_list\n            .save_current_image(Some(test_resources.file_folder().join(\"test2.png\")))\n            .unwrap();\n\n        assert!(image_list.current_image().unwrap().has_operations());\n    }\n}\n"
  },
  {
    "path": "src/image_operation.rs",
    "content": "use std::cmp;\n\nuse gtk::gdk_pixbuf::{InterpType, Pixbuf, PixbufRotation};\n\nuse crate::image::CoordinatesPair;\n\n#[derive(Copy, Clone, Debug)]\npub enum ImageOperation {\n    Rotate(PixbufRotation),\n    Crop(CoordinatesPair),\n    Resize((u32, u32)),\n}\n\npub trait ApplyImageOperation {\n    type Result;\n\n    fn apply_operation(self, image_operation: &ImageOperation) -> Self::Result;\n}\n\nimpl ApplyImageOperation for &Pixbuf {\n    type Result = Option<Pixbuf>;\n\n    fn apply_operation(self, image_operation: &ImageOperation) -> Self::Result {\n        match image_operation {\n            ImageOperation::Rotate(rotation) => self.rotate_simple(*rotation),\n            ImageOperation::Crop((\n                (start_position_x, start_position_y),\n                (end_position_x, end_position_y),\n            )) => {\n                let x = *cmp::min(start_position_x, end_position_x);\n                let y = *cmp::min(start_position_y, end_position_y);\n                let width = *cmp::max(start_position_x, end_position_x) - x;\n                let height = *cmp::max(start_position_y, end_position_y) - y;\n                self.new_subpixbuf(x as i32, y as i32, width as i32, height as i32)\n            }\n            ImageOperation::Resize((width, height)) => {\n                self.scale_simple(*width as i32, *height as i32, InterpType::Bilinear)\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::test_utils::TestResources;\n\n    use super::*;\n\n    const TEST_IMAGE: &[u8] = include_bytes!(\"resources/test/test_image.png\");\n\n    #[test]\n    fn test_apply_rotate_image_operation_on_pixbuf() {\n        let mut test_resources =\n            TestResources::new(\"test/test_apply_rotate_image_operation_on_pixbuf\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let pixbuf = Pixbuf::from_file(test_resources.file_folder().join(\"test.png\")).unwrap();\n        let image_operation = ImageOperation::Rotate(PixbufRotation::Clockwise);\n\n        assert_eq!(\n            pixbuf\n                .rotate_simple(PixbufRotation::Clockwise)\n                .unwrap()\n                .pixel_bytes(),\n            pixbuf\n                .apply_operation(&image_operation)\n                .unwrap()\n                .pixel_bytes()\n        );\n    }\n\n    #[test]\n    fn test_apply_crop_image_operation_on_pixbuf() {\n        let mut test_resources =\n            TestResources::new(\"test/test_apply_crop_image_operation_on_pixbuf\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let pixbuf = Pixbuf::from_file(test_resources.file_folder().join(\"test.png\")).unwrap();\n        let image_operation = ImageOperation::Crop(((10, 10), (20, 20)));\n\n        assert_eq!(\n            pixbuf.new_subpixbuf(10, 10, 10, 10).unwrap().pixel_bytes(),\n            pixbuf\n                .apply_operation(&image_operation)\n                .unwrap()\n                .pixel_bytes()\n        );\n    }\n\n    #[test]\n    fn test_apply_resize_image_operation_on_pixbuf() {\n        let mut test_resources =\n            TestResources::new(\"test/test_apply_resize_image_operation_on_pixbuf\");\n        test_resources.add_file(\"test.png\", TEST_IMAGE);\n\n        let pixbuf = Pixbuf::from_file(test_resources.file_folder().join(\"test.png\")).unwrap();\n        let image_operation = ImageOperation::Resize((10, 10));\n\n        assert_eq!(\n            pixbuf\n                .scale_simple(10, 10, InterpType::Bilinear)\n                .unwrap()\n                .pixel_bytes(),\n            pixbuf\n                .apply_operation(&image_operation)\n                .unwrap()\n                .pixel_bytes()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "use app::App;\nuse gtk::{gio::ApplicationFlags, prelude::*, Application};\n\n#[macro_use]\nextern crate log;\n\nmod app;\nmod file_list;\nmod image;\nmod image_list;\nmod image_operation;\nmod settings;\nmod ui;\n\n#[cfg(test)]\nmod test_utils;\n\nfn main() {\n    env_logger::init();\n\n    let application = Application::new(\n        Some(\"com.github.weclaw1.ImageRoll\"),\n        ApplicationFlags::HANDLES_OPEN | ApplicationFlags::NON_UNIQUE,\n    );\n\n    application.connect_activate(|app| {\n        App::create(app, None);\n    });\n\n    application.connect_open(move |app, files, _| {\n        App::create(app, Some(&files[0]));\n    });\n\n    application.run();\n}\n"
  },
  {
    "path": "src/resources/cargo-sources.json",
    "content": "[\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/aho-corasick/aho-corasick-0.7.18.crate\",\n        \"sha256\": \"1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f\",\n        \"dest\": \"cargo/vendor/aho-corasick-0.7.18\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/aho-corasick-0.7.18\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/anyhow/anyhow-1.0.58.crate\",\n        \"sha256\": \"bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704\",\n        \"dest\": \"cargo/vendor/anyhow-1.0.58\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/anyhow-1.0.58\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/ashpd/ashpd-0.3.2.crate\",\n        \"sha256\": \"6dcc8ed0b5211687437636d8c95f6a608f4281d142101b3b5d314b38bfadd40f\",\n        \"dest\": \"cargo/vendor/ashpd-0.3.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"6dcc8ed0b5211687437636d8c95f6a608f4281d142101b3b5d314b38bfadd40f\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/ashpd-0.3.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-broadcast/async-broadcast-0.3.4.crate\",\n        \"sha256\": \"90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b\",\n        \"dest\": \"cargo/vendor/async-broadcast-0.3.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-broadcast-0.3.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-channel/async-channel-1.6.1.crate\",\n        \"sha256\": \"2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319\",\n        \"dest\": \"cargo/vendor/async-channel-1.6.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-channel-1.6.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-executor/async-executor-1.4.1.crate\",\n        \"sha256\": \"871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965\",\n        \"dest\": \"cargo/vendor/async-executor-1.4.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-executor-1.4.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-io/async-io-1.6.0.crate\",\n        \"sha256\": \"a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b\",\n        \"dest\": \"cargo/vendor/async-io-1.6.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-io-1.6.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-lock/async-lock-2.4.0.crate\",\n        \"sha256\": \"e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b\",\n        \"dest\": \"cargo/vendor/async-lock-2.4.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-lock-2.4.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-recursion/async-recursion-0.3.2.crate\",\n        \"sha256\": \"d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2\",\n        \"dest\": \"cargo/vendor/async-recursion-0.3.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-recursion-0.3.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-task/async-task-4.1.0.crate\",\n        \"sha256\": \"677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8\",\n        \"dest\": \"cargo/vendor/async-task-4.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-task-4.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/async-trait/async-trait-0.1.52.crate\",\n        \"sha256\": \"061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3\",\n        \"dest\": \"cargo/vendor/async-trait-0.1.52\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/async-trait-0.1.52\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/atty/atty-0.2.14.crate\",\n        \"sha256\": \"d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8\",\n        \"dest\": \"cargo/vendor/atty-0.2.14\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/atty-0.2.14\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/autocfg/autocfg-1.0.1.crate\",\n        \"sha256\": \"cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a\",\n        \"dest\": \"cargo/vendor/autocfg-1.0.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/autocfg-1.0.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/bitflags/bitflags-1.3.2.crate\",\n        \"sha256\": \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\",\n        \"dest\": \"cargo/vendor/bitflags-1.3.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/bitflags-1.3.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/byteorder/byteorder-1.4.3.crate\",\n        \"sha256\": \"14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610\",\n        \"dest\": \"cargo/vendor/byteorder-1.4.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/byteorder-1.4.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cache-padded/cache-padded-1.2.0.crate\",\n        \"sha256\": \"c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c\",\n        \"dest\": \"cargo/vendor/cache-padded-1.2.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cache-padded-1.2.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cairo-rs/cairo-rs-0.15.1.crate\",\n        \"sha256\": \"b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a\",\n        \"dest\": \"cargo/vendor/cairo-rs-0.15.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cairo-rs-0.15.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cairo-sys-rs/cairo-sys-rs-0.15.1.crate\",\n        \"sha256\": \"3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8\",\n        \"dest\": \"cargo/vendor/cairo-sys-rs-0.15.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cairo-sys-rs-0.15.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cc/cc-1.0.73.crate\",\n        \"sha256\": \"2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11\",\n        \"dest\": \"cargo/vendor/cc-1.0.73\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cc-1.0.73\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cfb/cfb-0.7.3.crate\",\n        \"sha256\": \"d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f\",\n        \"dest\": \"cargo/vendor/cfb-0.7.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cfb-0.7.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cfg-expr/cfg-expr-0.10.1.crate\",\n        \"sha256\": \"295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd\",\n        \"dest\": \"cargo/vendor/cfg-expr-0.10.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cfg-expr-0.10.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/cfg-if/cfg-if-1.0.0.crate\",\n        \"sha256\": \"baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd\",\n        \"dest\": \"cargo/vendor/cfg-if-1.0.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/cfg-if-1.0.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/concurrent-queue/concurrent-queue-1.2.2.crate\",\n        \"sha256\": \"30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3\",\n        \"dest\": \"cargo/vendor/concurrent-queue-1.2.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/concurrent-queue-1.2.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/derivative/derivative-2.2.0.crate\",\n        \"sha256\": \"fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b\",\n        \"dest\": \"cargo/vendor/derivative-2.2.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/derivative-2.2.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/easy-parallel/easy-parallel-3.2.0.crate\",\n        \"sha256\": \"6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946\",\n        \"dest\": \"cargo/vendor/easy-parallel-3.2.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/easy-parallel-3.2.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/either/either-1.6.1.crate\",\n        \"sha256\": \"e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457\",\n        \"dest\": \"cargo/vendor/either-1.6.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/either-1.6.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/enumflags2/enumflags2-0.7.3.crate\",\n        \"sha256\": \"a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def\",\n        \"dest\": \"cargo/vendor/enumflags2-0.7.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/enumflags2-0.7.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/enumflags2_derive/enumflags2_derive-0.7.3.crate\",\n        \"sha256\": \"144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4\",\n        \"dest\": \"cargo/vendor/enumflags2_derive-0.7.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/enumflags2_derive-0.7.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/env_logger/env_logger-0.9.0.crate\",\n        \"sha256\": \"0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3\",\n        \"dest\": \"cargo/vendor/env_logger-0.9.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/env_logger-0.9.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/event-listener/event-listener-2.5.2.crate\",\n        \"sha256\": \"77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71\",\n        \"dest\": \"cargo/vendor/event-listener-2.5.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/event-listener-2.5.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/fastrand/fastrand-1.7.0.crate\",\n        \"sha256\": \"c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf\",\n        \"dest\": \"cargo/vendor/fastrand-1.7.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/fastrand-1.7.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/field-offset/field-offset-0.3.4.crate\",\n        \"sha256\": \"1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92\",\n        \"dest\": \"cargo/vendor/field-offset-0.3.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/field-offset-0.3.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/fnv/fnv-1.0.7.crate\",\n        \"sha256\": \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\",\n        \"dest\": \"cargo/vendor/fnv-1.0.7\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/fnv-1.0.7\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures/futures-0.3.16.crate\",\n        \"sha256\": \"1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b\",\n        \"dest\": \"cargo/vendor/futures-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-channel/futures-channel-0.3.16.crate\",\n        \"sha256\": \"74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9\",\n        \"dest\": \"cargo/vendor/futures-channel-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-channel-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-core/futures-core-0.3.16.crate\",\n        \"sha256\": \"af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99\",\n        \"dest\": \"cargo/vendor/futures-core-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-core-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-executor/futures-executor-0.3.16.crate\",\n        \"sha256\": \"4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c\",\n        \"dest\": \"cargo/vendor/futures-executor-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-executor-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-io/futures-io-0.3.16.crate\",\n        \"sha256\": \"0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582\",\n        \"dest\": \"cargo/vendor/futures-io-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-io-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-lite/futures-lite-1.12.0.crate\",\n        \"sha256\": \"7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48\",\n        \"dest\": \"cargo/vendor/futures-lite-1.12.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-lite-1.12.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-macro/futures-macro-0.3.16.crate\",\n        \"sha256\": \"c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57\",\n        \"dest\": \"cargo/vendor/futures-macro-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-macro-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-sink/futures-sink-0.3.21.crate\",\n        \"sha256\": \"21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868\",\n        \"dest\": \"cargo/vendor/futures-sink-0.3.21\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-sink-0.3.21\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-task/futures-task-0.3.16.crate\",\n        \"sha256\": \"bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2\",\n        \"dest\": \"cargo/vendor/futures-task-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-task-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/futures-util/futures-util-0.3.16.crate\",\n        \"sha256\": \"67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78\",\n        \"dest\": \"cargo/vendor/futures-util-0.3.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/futures-util-0.3.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gdk-pixbuf/gdk-pixbuf-0.15.4.crate\",\n        \"sha256\": \"73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49\",\n        \"dest\": \"cargo/vendor/gdk-pixbuf-0.15.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gdk-pixbuf-0.15.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gdk-pixbuf-sys/gdk-pixbuf-sys-0.15.1.crate\",\n        \"sha256\": \"413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171\",\n        \"dest\": \"cargo/vendor/gdk-pixbuf-sys-0.15.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gdk-pixbuf-sys-0.15.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gdk4/gdk4-0.4.8.crate\",\n        \"sha256\": \"4fabb7cf843c26b085a5d68abb95d0c0bf27a9ae2eeff9c4adb503a1eb580876\",\n        \"dest\": \"cargo/vendor/gdk4-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"4fabb7cf843c26b085a5d68abb95d0c0bf27a9ae2eeff9c4adb503a1eb580876\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gdk4-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gdk4-sys/gdk4-sys-0.4.8.crate\",\n        \"sha256\": \"efe7dcb44f5c00aeabff3f69abfc5673de46559070f89bd3fbb7b66485d9cef2\",\n        \"dest\": \"cargo/vendor/gdk4-sys-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"efe7dcb44f5c00aeabff3f69abfc5673de46559070f89bd3fbb7b66485d9cef2\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gdk4-sys-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/getrandom/getrandom-0.2.4.crate\",\n        \"sha256\": \"418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c\",\n        \"dest\": \"cargo/vendor/getrandom-0.2.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/getrandom-0.2.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gio/gio-0.15.5.crate\",\n        \"sha256\": \"59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf\",\n        \"dest\": \"cargo/vendor/gio-0.15.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gio-0.15.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gio-sys/gio-sys-0.15.5.crate\",\n        \"sha256\": \"4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3\",\n        \"dest\": \"cargo/vendor/gio-sys-0.15.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gio-sys-0.15.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/glib/glib-0.15.5.crate\",\n        \"sha256\": \"41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7\",\n        \"dest\": \"cargo/vendor/glib-0.15.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/glib-0.15.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/glib-macros/glib-macros-0.15.3.crate\",\n        \"sha256\": \"e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1\",\n        \"dest\": \"cargo/vendor/glib-macros-0.15.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/glib-macros-0.15.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/glib-sys/glib-sys-0.15.5.crate\",\n        \"sha256\": \"fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c\",\n        \"dest\": \"cargo/vendor/glib-sys-0.15.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/glib-sys-0.15.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gobject-sys/gobject-sys-0.15.5.crate\",\n        \"sha256\": \"df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a\",\n        \"dest\": \"cargo/vendor/gobject-sys-0.15.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gobject-sys-0.15.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/graphene-rs/graphene-rs-0.15.1.crate\",\n        \"sha256\": \"7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570\",\n        \"dest\": \"cargo/vendor/graphene-rs-0.15.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/graphene-rs-0.15.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/graphene-sys/graphene-sys-0.15.10.crate\",\n        \"sha256\": \"fa691fc7337ba1df599afb55c3bcb85c04f1b3f17362570e9bb0ff0d1bc3028a\",\n        \"dest\": \"cargo/vendor/graphene-sys-0.15.10\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fa691fc7337ba1df599afb55c3bcb85c04f1b3f17362570e9bb0ff0d1bc3028a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/graphene-sys-0.15.10\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gsk4/gsk4-0.4.8.crate\",\n        \"sha256\": \"05e9020d333280b3aa38d496495bfa9b50712eebf1ad63f0ec5bcddb5eb61be4\",\n        \"dest\": \"cargo/vendor/gsk4-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"05e9020d333280b3aa38d496495bfa9b50712eebf1ad63f0ec5bcddb5eb61be4\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gsk4-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gsk4-sys/gsk4-sys-0.4.8.crate\",\n        \"sha256\": \"7add39ccf60078508c838643a2dcc91f045c46ed63b5ea6ab701b2e25bda3fea\",\n        \"dest\": \"cargo/vendor/gsk4-sys-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7add39ccf60078508c838643a2dcc91f045c46ed63b5ea6ab701b2e25bda3fea\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gsk4-sys-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gtk4/gtk4-0.4.8.crate\",\n        \"sha256\": \"c64f0c2a3d80e899dc3febddad5bac193ffcf74a0fd7e31037f30dd34d6f7396\",\n        \"dest\": \"cargo/vendor/gtk4-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c64f0c2a3d80e899dc3febddad5bac193ffcf74a0fd7e31037f30dd34d6f7396\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gtk4-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gtk4-macros/gtk4-macros-0.4.8.crate\",\n        \"sha256\": \"fafbcc920af4eb677d7d164853e7040b9de5a22379c596f570190c675d45f7a7\",\n        \"dest\": \"cargo/vendor/gtk4-macros-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fafbcc920af4eb677d7d164853e7040b9de5a22379c596f570190c675d45f7a7\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gtk4-macros-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/gtk4-sys/gtk4-sys-0.4.8.crate\",\n        \"sha256\": \"5bc8006eea634b7c72da3ff79e24606e45f21b3b832a3c5a1f543f5f97eb0f63\",\n        \"dest\": \"cargo/vendor/gtk4-sys-0.4.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"5bc8006eea634b7c72da3ff79e24606e45f21b3b832a3c5a1f543f5f97eb0f63\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/gtk4-sys-0.4.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/heck/heck-0.4.0.crate\",\n        \"sha256\": \"2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9\",\n        \"dest\": \"cargo/vendor/heck-0.4.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/heck-0.4.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/hermit-abi/hermit-abi-0.1.19.crate\",\n        \"sha256\": \"62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33\",\n        \"dest\": \"cargo/vendor/hermit-abi-0.1.19\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/hermit-abi-0.1.19\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/hex/hex-0.4.3.crate\",\n        \"sha256\": \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\",\n        \"dest\": \"cargo/vendor/hex-0.4.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/hex-0.4.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/humantime/humantime-2.1.0.crate\",\n        \"sha256\": \"9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4\",\n        \"dest\": \"cargo/vendor/humantime-2.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/humantime-2.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/infer/infer-0.9.0.crate\",\n        \"sha256\": \"f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a\",\n        \"dest\": \"cargo/vendor/infer-0.9.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/infer-0.9.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/instant/instant-0.1.12.crate\",\n        \"sha256\": \"7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c\",\n        \"dest\": \"cargo/vendor/instant-0.1.12\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/instant-0.1.12\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/itertools/itertools-0.10.3.crate\",\n        \"sha256\": \"a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3\",\n        \"dest\": \"cargo/vendor/itertools-0.10.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/itertools-0.10.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/lazy_static/lazy_static-1.4.0.crate\",\n        \"sha256\": \"e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646\",\n        \"dest\": \"cargo/vendor/lazy_static-1.4.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/lazy_static-1.4.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/libc/libc-0.2.118.crate\",\n        \"sha256\": \"06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94\",\n        \"dest\": \"cargo/vendor/libc-0.2.118\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/libc-0.2.118\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/log/log-0.4.17.crate\",\n        \"sha256\": \"abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e\",\n        \"dest\": \"cargo/vendor/log-0.4.17\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/log-0.4.17\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/memchr/memchr-2.4.0.crate\",\n        \"sha256\": \"b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc\",\n        \"dest\": \"cargo/vendor/memchr-2.4.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/memchr-2.4.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/memoffset/memoffset-0.6.4.crate\",\n        \"sha256\": \"59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9\",\n        \"dest\": \"cargo/vendor/memoffset-0.6.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/memoffset-0.6.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/nix/nix-0.23.1.crate\",\n        \"sha256\": \"9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6\",\n        \"dest\": \"cargo/vendor/nix-0.23.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/nix-0.23.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/once_cell/once_cell-1.8.0.crate\",\n        \"sha256\": \"692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56\",\n        \"dest\": \"cargo/vendor/once_cell-1.8.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/once_cell-1.8.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/ordered-stream/ordered-stream-0.0.1.crate\",\n        \"sha256\": \"44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1\",\n        \"dest\": \"cargo/vendor/ordered-stream-0.0.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/ordered-stream-0.0.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/pango/pango-0.15.2.crate\",\n        \"sha256\": \"79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94\",\n        \"dest\": \"cargo/vendor/pango-0.15.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/pango-0.15.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/pango-sys/pango-sys-0.15.1.crate\",\n        \"sha256\": \"7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2\",\n        \"dest\": \"cargo/vendor/pango-sys-0.15.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/pango-sys-0.15.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/parking/parking-2.0.0.crate\",\n        \"sha256\": \"427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72\",\n        \"dest\": \"cargo/vendor/parking-2.0.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/parking-2.0.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/pest/pest-2.1.3.crate\",\n        \"sha256\": \"10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53\",\n        \"dest\": \"cargo/vendor/pest-2.1.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/pest-2.1.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.7.crate\",\n        \"sha256\": \"8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443\",\n        \"dest\": \"cargo/vendor/pin-project-lite-0.2.7\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/pin-project-lite-0.2.7\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/pin-utils/pin-utils-0.1.0.crate\",\n        \"sha256\": \"8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184\",\n        \"dest\": \"cargo/vendor/pin-utils-0.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/pin-utils-0.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/pkg-config/pkg-config-0.3.25.crate\",\n        \"sha256\": \"1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae\",\n        \"dest\": \"cargo/vendor/pkg-config-0.3.25\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/pkg-config-0.3.25\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/polling/polling-2.2.0.crate\",\n        \"sha256\": \"685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259\",\n        \"dest\": \"cargo/vendor/polling-2.2.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/polling-2.2.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.16.crate\",\n        \"sha256\": \"eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872\",\n        \"dest\": \"cargo/vendor/ppv-lite86-0.2.16\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/ppv-lite86-0.2.16\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/proc-macro-crate/proc-macro-crate-1.0.0.crate\",\n        \"sha256\": \"41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92\",\n        \"dest\": \"cargo/vendor/proc-macro-crate-1.0.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/proc-macro-crate-1.0.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/proc-macro-error/proc-macro-error-1.0.4.crate\",\n        \"sha256\": \"da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c\",\n        \"dest\": \"cargo/vendor/proc-macro-error-1.0.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/proc-macro-error-1.0.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/proc-macro-error-attr/proc-macro-error-attr-1.0.4.crate\",\n        \"sha256\": \"a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869\",\n        \"dest\": \"cargo/vendor/proc-macro-error-attr-1.0.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/proc-macro-error-attr-1.0.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/proc-macro-hack/proc-macro-hack-0.5.19.crate\",\n        \"sha256\": \"dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5\",\n        \"dest\": \"cargo/vendor/proc-macro-hack-0.5.19\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/proc-macro-hack-0.5.19\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/proc-macro-nested/proc-macro-nested-0.1.7.crate\",\n        \"sha256\": \"bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086\",\n        \"dest\": \"cargo/vendor/proc-macro-nested-0.1.7\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/proc-macro-nested-0.1.7\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.28.crate\",\n        \"sha256\": \"5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612\",\n        \"dest\": \"cargo/vendor/proc-macro2-1.0.28\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/proc-macro2-1.0.28\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/quick-xml/quick-xml-0.22.0.crate\",\n        \"sha256\": \"8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b\",\n        \"dest\": \"cargo/vendor/quick-xml-0.22.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/quick-xml-0.22.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/quote/quote-1.0.9.crate\",\n        \"sha256\": \"c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7\",\n        \"dest\": \"cargo/vendor/quote-1.0.9\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/quote-1.0.9\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/rand/rand-0.8.5.crate\",\n        \"sha256\": \"34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404\",\n        \"dest\": \"cargo/vendor/rand-0.8.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/rand-0.8.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate\",\n        \"sha256\": \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\",\n        \"dest\": \"cargo/vendor/rand_chacha-0.3.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/rand_chacha-0.3.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/rand_core/rand_core-0.6.3.crate\",\n        \"sha256\": \"d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7\",\n        \"dest\": \"cargo/vendor/rand_core-0.6.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/rand_core-0.6.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/regex/regex-1.5.4.crate\",\n        \"sha256\": \"d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461\",\n        \"dest\": \"cargo/vendor/regex-1.5.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/regex-1.5.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/regex-syntax/regex-syntax-0.6.25.crate\",\n        \"sha256\": \"f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b\",\n        \"dest\": \"cargo/vendor/regex-syntax-0.6.25\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/regex-syntax-0.6.25\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/rustc_version/rustc_version-0.3.3.crate\",\n        \"sha256\": \"f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee\",\n        \"dest\": \"cargo/vendor/rustc_version-0.3.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/rustc_version-0.3.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/semver/semver-0.11.0.crate\",\n        \"sha256\": \"f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6\",\n        \"dest\": \"cargo/vendor/semver-0.11.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/semver-0.11.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/semver-parser/semver-parser-0.10.2.crate\",\n        \"sha256\": \"00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7\",\n        \"dest\": \"cargo/vendor/semver-parser-0.10.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/semver-parser-0.10.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/serde/serde-1.0.127.crate\",\n        \"sha256\": \"f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8\",\n        \"dest\": \"cargo/vendor/serde-1.0.127\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/serde-1.0.127\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/serde_derive/serde_derive-1.0.127.crate\",\n        \"sha256\": \"a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc\",\n        \"dest\": \"cargo/vendor/serde_derive-1.0.127\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/serde_derive-1.0.127\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/serde_repr/serde_repr-0.1.7.crate\",\n        \"sha256\": \"98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5\",\n        \"dest\": \"cargo/vendor/serde_repr-0.1.7\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/serde_repr-0.1.7\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/sha1/sha1-0.6.1.crate\",\n        \"sha256\": \"c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770\",\n        \"dest\": \"cargo/vendor/sha1-0.6.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/sha1-0.6.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/sha1_smol/sha1_smol-1.0.0.crate\",\n        \"sha256\": \"ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012\",\n        \"dest\": \"cargo/vendor/sha1_smol-1.0.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/sha1_smol-1.0.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/slab/slab-0.4.4.crate\",\n        \"sha256\": \"c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590\",\n        \"dest\": \"cargo/vendor/slab-0.4.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/slab-0.4.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/smallvec/smallvec-1.6.1.crate\",\n        \"sha256\": \"fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e\",\n        \"dest\": \"cargo/vendor/smallvec-1.6.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/smallvec-1.6.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/socket2/socket2-0.4.4.crate\",\n        \"sha256\": \"66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0\",\n        \"dest\": \"cargo/vendor/socket2-0.4.4\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/socket2-0.4.4\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/static_assertions/static_assertions-1.1.0.crate\",\n        \"sha256\": \"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\",\n        \"dest\": \"cargo/vendor/static_assertions-1.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/static_assertions-1.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/syn/syn-1.0.74.crate\",\n        \"sha256\": \"1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c\",\n        \"dest\": \"cargo/vendor/syn-1.0.74\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/syn-1.0.74\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/system-deps/system-deps-6.0.2.crate\",\n        \"sha256\": \"a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709\",\n        \"dest\": \"cargo/vendor/system-deps-6.0.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/system-deps-6.0.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/termcolor/termcolor-1.1.2.crate\",\n        \"sha256\": \"2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4\",\n        \"dest\": \"cargo/vendor/termcolor-1.1.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/termcolor-1.1.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/thiserror/thiserror-1.0.26.crate\",\n        \"sha256\": \"93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2\",\n        \"dest\": \"cargo/vendor/thiserror-1.0.26\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/thiserror-1.0.26\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.26.crate\",\n        \"sha256\": \"060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745\",\n        \"dest\": \"cargo/vendor/thiserror-impl-1.0.26\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/thiserror-impl-1.0.26\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/toml/toml-0.5.8.crate\",\n        \"sha256\": \"a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa\",\n        \"dest\": \"cargo/vendor/toml-0.5.8\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/toml-0.5.8\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/ucd-trie/ucd-trie-0.1.3.crate\",\n        \"sha256\": \"56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c\",\n        \"dest\": \"cargo/vendor/ucd-trie-0.1.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/ucd-trie-0.1.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/unicode-xid/unicode-xid-0.2.2.crate\",\n        \"sha256\": \"8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3\",\n        \"dest\": \"cargo/vendor/unicode-xid-0.2.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/unicode-xid-0.2.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/uuid/uuid-1.1.2.crate\",\n        \"sha256\": \"dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f\",\n        \"dest\": \"cargo/vendor/uuid-1.1.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/uuid-1.1.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/version-compare/version-compare-0.1.0.crate\",\n        \"sha256\": \"fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73\",\n        \"dest\": \"cargo/vendor/version-compare-0.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/version-compare-0.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/version_check/version_check-0.9.3.crate\",\n        \"sha256\": \"5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe\",\n        \"dest\": \"cargo/vendor/version_check-0.9.3\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/version_check-0.9.3\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/waker-fn/waker-fn-1.1.0.crate\",\n        \"sha256\": \"9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca\",\n        \"dest\": \"cargo/vendor/waker-fn-1.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/waker-fn-1.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/wasi/wasi-0.10.2+wasi-snapshot-preview1.crate\",\n        \"sha256\": \"fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6\",\n        \"dest\": \"cargo/vendor/wasi-0.10.2+wasi-snapshot-preview1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/wasi-0.10.2+wasi-snapshot-preview1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/wepoll-ffi/wepoll-ffi-0.1.2.crate\",\n        \"sha256\": \"d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb\",\n        \"dest\": \"cargo/vendor/wepoll-ffi-0.1.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/wepoll-ffi-0.1.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/winapi/winapi-0.3.9.crate\",\n        \"sha256\": \"5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419\",\n        \"dest\": \"cargo/vendor/winapi-0.3.9\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/winapi-0.3.9\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/winapi-i686-pc-windows-gnu/winapi-i686-pc-windows-gnu-0.4.0.crate\",\n        \"sha256\": \"ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6\",\n        \"dest\": \"cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/winapi-util/winapi-util-0.1.5.crate\",\n        \"sha256\": \"70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178\",\n        \"dest\": \"cargo/vendor/winapi-util-0.1.5\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/winapi-util-0.1.5\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/winapi-x86_64-pc-windows-gnu/winapi-x86_64-pc-windows-gnu-0.4.0.crate\",\n        \"sha256\": \"712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f\",\n        \"dest\": \"cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/zbus/zbus-2.1.1.crate\",\n        \"sha256\": \"7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a\",\n        \"dest\": \"cargo/vendor/zbus-2.1.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/zbus-2.1.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/zbus_macros/zbus_macros-2.1.1.crate\",\n        \"sha256\": \"36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040\",\n        \"dest\": \"cargo/vendor/zbus_macros-2.1.1\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/zbus_macros-2.1.1\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/zbus_names/zbus_names-2.1.0.crate\",\n        \"sha256\": \"45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1\",\n        \"dest\": \"cargo/vendor/zbus_names-2.1.0\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/zbus_names-2.1.0\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/zvariant/zvariant-3.1.2.crate\",\n        \"sha256\": \"49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033\",\n        \"dest\": \"cargo/vendor/zvariant-3.1.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/zvariant-3.1.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/zvariant_derive/zvariant_derive-3.1.2.crate\",\n        \"sha256\": \"8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a\",\n        \"dest\": \"cargo/vendor/zvariant_derive-3.1.2\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"{\\\"package\\\": \\\"8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a\\\", \\\"files\\\": {}}\",\n        \"dest\": \"cargo/vendor/zvariant_derive-3.1.2\",\n        \"dest-filename\": \".cargo-checksum.json\"\n    },\n    {\n        \"type\": \"inline\",\n        \"contents\": \"[source.vendored-sources]\\ndirectory = \\\"cargo/vendor\\\"\\n\\n[source.crates-io]\\nreplace-with = \\\"vendored-sources\\\"\\n\",\n        \"dest\": \"cargo\",\n        \"dest-filename\": \"config\"\n    }\n]"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.desktop",
    "content": "[Desktop Entry]\nType=Application\nName=Image Roll\nComment=Image viewer with basic image manipulation tools\nExec=image-roll %U\nIcon=com.github.weclaw1.ImageRoll\nTerminal=false\nStartupWMClass=image-roll\nTryExec=image-roll\nCategories=Graphics;\nX-Purism-FormFactor=Workstation;Mobile;\nMimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/tiff;image/x-bmp;image/x-gray;image/x-icb;image/x-ico;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-xbitmap;image/x-xpixmap;image/x-pcx;image/svg+xml;image/svg+xml-compressed;image/vnd.wap.wbmp;image/x-icns;"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.gschema.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<schemalist>\n  <schema id=\"com.github.weclaw1.ImageRoll\" path=\"/com/github/weclaw1/ImageRoll/\">\n    <key name=\"window-width\" type=\"u\">\n      <default>1024</default>\n      <summary>Last window width</summary>\n    </key>\n    <key name=\"window-height\" type=\"u\">\n      <default>768</default>\n      <summary>Last window height</summary>\n    </key>\n  </schema>\n</schemalist>\n"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.metainfo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop-application\">\n  <id>com.github.weclaw1.ImageRoll</id>\n  <name>Image Roll</name>\n  <summary>Image viewer with basic image manipulation tools</summary>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>MIT</project_license>\n  <description>\n    <p>\n      Image Roll is a simple and fast GTK image viewer with basic image manipulation tools. Written in rust.\n    </p>\n  </description>\n  <launchable type=\"desktop-id\">com.github.weclaw1.ImageRoll.desktop</launchable>\n  <screenshots>\n    <screenshot type=\"default\">\n      <image>https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/screenshot.png</image>\n    </screenshot>\n  </screenshots>\n  <releases>\n    <release version=\"2.1.0\" date=\"2022-07-08\"/>\n  </releases>\n  <content_rating type=\"oars-1.0\"/>\n  <developer_name>Robert Węcławski</developer_name>\n  <url type=\"homepage\">https://github.com/weclaw1/image-roll</url>\n  <url type=\"bugtracker\">https://github.com/weclaw1/image-roll/issues</url>\n</component>"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.yaml",
    "content": "app-id: com.github.weclaw1.ImageRoll\nruntime: org.gnome.Platform\nruntime-version: '42'\nsdk: org.gnome.Sdk\nsdk-extensions:\n- org.freedesktop.Sdk.Extension.rust-stable\ncommand: image-roll\nfinish-args:\n- --share=ipc\n- --socket=fallback-x11\n- --socket=wayland\n- --filesystem=home\n- --filesystem=/mnt\n- --filesystem=/media\n- --filesystem=/run/media\n- --device=dri\nbuild-options:\n  append-path: /usr/lib/sdk/rust-stable/bin\n  env:\n    CARGO_HOME: /run/build/image-roll/cargo\nmodules:\n- name: image-roll\n  buildsystem: simple\n  build-commands:\n  - cargo --offline fetch --manifest-path Cargo.toml\n  - cargo --offline build --release\n  - install -Dm755 ./target/release/image-roll -t /app/bin/\n  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.svg -t /app/share/icons/hicolor/scalable/apps/\n  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.desktop -t /app/share/applications/\n  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.metainfo.xml -t /app/share/metainfo/\n  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.gschema.xml -t /app/share/glib-2.0/schemas/\n  - glib-compile-schemas /app/share/glib-2.0/schemas\n  sources:\n  - cargo-sources.json\n  - type: git\n    url: https://github.com/weclaw1/image-roll.git\n    tag: 2.1.0\n"
  },
  {
    "path": "src/resources/image-roll.cmb",
    "content": "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n<!DOCTYPE cambalache-project SYSTEM \"cambalache-project.dtd\">\n<cambalache-project version=\"0.9.1\" target_tk=\"gtk-4.0\">\n  <ui>\n\t(1,None,\"image-roll.ui\",\"image-roll.ui\",None,None,None,None,None,None)\n  </ui>\n  <object>\n\t(1,1,\"GtkApplicationWindow\",\"main_window\",None,None,None,None,None),\n\t(1,2,\"GtkHeaderBar\",\"headerbar\",1,None,\"titlebar\",None,-1),\n\t(1,3,\"GtkBox\",None,2,None,\"end\",None,None),\n\t(1,4,\"GtkBox\",None,2,None,\"start\",None,None),\n\t(1,5,\"GtkButton\",\"preview_smaller_button\",4,None,None,None,None),\n\t(1,6,\"GtkLabel\",\"preview_size_label\",4,None,None,None,1),\n\t(1,7,\"GtkButton\",\"preview_larger_button\",4,None,None,None,2),\n\t(1,8,\"GtkButton\",\"preview_fit_screen_button\",4,None,None,None,3),\n\t(1,9,\"GtkButton\",\"delete_button\",3,None,None,None,None),\n\t(1,10,\"GtkMenuButton\",\"menu_button\",3,None,None,None,1),\n\t(1,11,\"GtkPopoverMenu\",\"popover_menu\",None,None,None,None,None),\n\t(1,12,\"GtkBox\",None,11,None,None,None,None),\n\t(1,13,\"GtkButton\",\"open_menu_button\",12,None,None,None,None),\n\t(1,14,\"GtkButton\",\"save_menu_button\",12,None,None,None,1),\n\t(1,15,\"GtkButton\",\"set_as_wallpaper_menu_button\",12,None,None,None,4),\n\t(1,16,\"GtkButton\",\"print_menu_button\",12,None,None,None,5),\n\t(1,17,\"GtkButton\",\"copy_menu_button\",12,None,None,None,3),\n\t(1,18,\"GtkButton\",\"save_as_menu_button\",12,None,None,None,2),\n\t(1,23,\"GtkBox\",None,1,None,None,None,None),\n\t(1,24,\"GtkInfoBar\",\"error_info_bar\",23,None,None,None,None),\n\t(1,25,\"GtkLabel\",\"error_info_bar_text\",24,None,None,None,None),\n\t(1,26,\"GtkScrolledWindow\",\"image_scrolled_window\",23,None,None,None,1),\n\t(1,27,\"GtkViewport\",\"image_viewport\",26,None,None,None,None),\n\t(1,29,\"GtkBox\",\"action_bar\",23,None,None,None,2),\n\t(1,30,\"GtkButton\",\"previous_button\",29,None,None,None,None),\n\t(1,31,\"GtkFlowBox\",None,29,None,None,None,1),\n\t(1,32,\"GtkButton\",\"next_button\",29,None,None,None,2),\n\t(1,33,\"GtkButton\",\"undo_button\",31,None,None,None,None),\n\t(1,34,\"GtkButton\",\"rotate_counterclockwise_button\",31,None,None,None,1),\n\t(1,36,\"GtkMenuButton\",\"resize_button\",31,None,None,None,3),\n\t(1,37,\"GtkToggleButton\",\"crop_button\",31,None,None,None,2),\n\t(1,38,\"GtkPopover\",\"resize_popover\",None,None,None,None,None),\n\t(1,39,\"GtkBox\",None,38,None,None,None,None),\n\t(1,40,\"GtkToggleButton\",\"link_aspect_ratio_button\",39,None,None,None,None),\n\t(1,41,\"GtkSpinButton\",\"width_spin_button\",39,None,None,None,1),\n\t(1,42,\"GtkAdjustment\",\"width_adjustment\",None,None,None,None,None),\n\t(1,43,\"GtkLabel\",\"x_label\",39,None,None,None,2),\n\t(1,44,\"GtkSpinButton\",\"height_spin_button\",39,None,None,None,3),\n\t(1,45,\"GtkAdjustment\",\"height_adjustment\",None,None,None,None,None),\n\t(1,46,\"GtkButton\",\"apply_resize_button\",39,None,None,None,4),\n\t(1,47,\"GtkButton\",\"rotate_clockwise_button\",31,None,None,None,4),\n\t(1,48,\"GtkButton\",\"redo_button\",31,None,None,None,5),\n\t(1,49,\"GtkDrawingArea\",\"image_widget\",27,None,None,None,None)\n  </object>\n  <object_property>\n\t(1,1,\"GtkWindow\",\"child\",None,None,None,None,None,23),\n\t(1,1,\"GtkWindow\",\"default-height\",\"768\",None,None,None,None,None),\n\t(1,1,\"GtkWindow\",\"default-width\",\"1024\",None,None,None,None,None),\n\t(1,1,\"GtkWindow\",\"icon-name\",\"com.github.weclaw1.ImageRoll\",None,None,None,None,None),\n\t(1,1,\"GtkWindow\",\"title\",\"Image Roll\",None,None,None,None,None),\n\t(1,3,\"GtkBox\",\"spacing\",\"5\",None,None,None,None,None),\n\t(1,5,\"GtkButton\",\"icon-name\",\"zoom-out-symbolic\",None,None,None,None,None),\n\t(1,6,\"GtkLabel\",\"label\",\"Fit screen\",None,None,None,None,None),\n\t(1,7,\"GtkButton\",\"icon-name\",\"zoom-in-symbolic\",None,None,None,None,None),\n\t(1,8,\"GtkButton\",\"icon-name\",\"zoom-fit-best-symbolic\",None,None,None,None,None),\n\t(1,9,\"GtkButton\",\"icon-name\",\"user-trash-symbolic\",None,None,None,None,None),\n\t(1,10,\"GtkMenuButton\",\"direction\",\"none\",None,None,None,None,None),\n\t(1,10,\"GtkMenuButton\",\"popover\",\"11\",None,None,None,None,None),\n\t(1,11,\"GtkPopover\",\"child\",None,None,None,None,None,12),\n\t(1,12,\"GtkOrientable\",\"orientation\",\"vertical\",None,None,None,None,None),\n\t(1,13,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,13,\"GtkButton\",\"label\",\"Open...\",None,None,None,None,None),\n\t(1,14,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,14,\"GtkButton\",\"label\",\"Save\",None,None,None,None,None),\n\t(1,15,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,15,\"GtkButton\",\"label\",\"Set as wallpaper\",None,None,None,None,None),\n\t(1,16,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,16,\"GtkButton\",\"label\",\"Print\",None,None,None,None,None),\n\t(1,17,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,17,\"GtkButton\",\"label\",\"Copy\",None,None,None,None,None),\n\t(1,18,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,18,\"GtkButton\",\"label\",\"Save as...\",None,None,None,None,None),\n\t(1,23,\"GtkOrientable\",\"orientation\",\"vertical\",None,None,None,None,None),\n\t(1,24,\"GtkInfoBar\",\"message-type\",\"error\",None,None,None,None,None),\n\t(1,24,\"GtkInfoBar\",\"revealed\",\"False\",None,None,None,None,None),\n\t(1,24,\"GtkInfoBar\",\"show-close-button\",\"True\",None,None,None,None,None),\n\t(1,25,\"GtkLabel\",\"label\",\"ERROR\",None,None,None,None,None),\n\t(1,26,\"GtkScrolledWindow\",\"child\",None,None,None,None,None,27),\n\t(1,26,\"GtkWidget\",\"vexpand\",\"True\",None,None,None,None,None),\n\t(1,27,\"GtkViewport\",\"child\",None,None,None,None,None,49),\n\t(1,30,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,30,\"GtkButton\",\"icon-name\",\"go-previous-symbolic\",None,None,None,None,None),\n\t(1,30,\"GtkWidget\",\"halign\",\"start\",None,None,None,None,None),\n\t(1,30,\"GtkWidget\",\"hexpand\",\"True\",None,None,None,None,None),\n\t(1,31,\"GtkFlowBox\",\"column-spacing\",\"8\",None,None,None,None,None),\n\t(1,31,\"GtkFlowBox\",\"max-children-per-line\",\"6\",None,None,None,None,None),\n\t(1,31,\"GtkWidget\",\"halign\",\"center\",None,None,None,None,None),\n\t(1,31,\"GtkWidget\",\"width-request\",\"300\",None,None,None,None,None),\n\t(1,32,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,32,\"GtkButton\",\"icon-name\",\"go-next-symbolic\",None,None,None,None,None),\n\t(1,32,\"GtkWidget\",\"halign\",\"end\",None,None,None,None,None),\n\t(1,32,\"GtkWidget\",\"hexpand\",\"True\",None,None,None,None,None),\n\t(1,33,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,33,\"GtkButton\",\"icon-name\",\"edit-undo-symbolic\",None,None,None,None,None),\n\t(1,34,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,34,\"GtkButton\",\"icon-name\",\"object-rotate-left-symbolic\",None,None,None,None,None),\n\t(1,36,\"GtkMenuButton\",\"direction\",\"up\",None,None,None,None,None),\n\t(1,36,\"GtkMenuButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,36,\"GtkMenuButton\",\"icon-name\",\"view-fullscreen-symbolic\",None,None,None,None,None),\n\t(1,36,\"GtkMenuButton\",\"popover\",\"38\",None,None,None,None,None),\n\t(1,37,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,37,\"GtkButton\",\"icon-name\",\"crop-symbolic\",None,None,None,None,None),\n\t(1,38,\"GtkPopover\",\"child\",None,None,None,None,None,39),\n\t(1,38,\"GtkPopover\",\"position\",\"top\",None,None,None,None,None),\n\t(1,40,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,40,\"GtkButton\",\"icon-name\",\"insert-link-symbolic\",None,None,None,None,None),\n\t(1,41,\"GtkOrientable\",\"orientation\",\"vertical\",None,None,None,None,None),\n\t(1,41,\"GtkSpinButton\",\"adjustment\",\"42\",None,None,None,None,None),\n\t(1,41,\"GtkSpinButton\",\"climb-rate\",\"0.5\",None,None,None,None,None),\n\t(1,42,\"GtkAdjustment\",\"page-increment\",\"10.0\",None,None,None,None,None),\n\t(1,42,\"GtkAdjustment\",\"step-increment\",\"1.0\",None,None,None,None,None),\n\t(1,42,\"GtkAdjustment\",\"upper\",\"2147483647.0\",None,None,None,None,None),\n\t(1,43,\"GtkLabel\",\"label\",\"x\",None,None,None,None,None),\n\t(1,43,\"GtkWidget\",\"margin-end\",\"5\",None,None,None,None,None),\n\t(1,43,\"GtkWidget\",\"margin-start\",\"5\",None,None,None,None,None),\n\t(1,44,\"GtkOrientable\",\"orientation\",\"vertical\",None,None,None,None,None),\n\t(1,44,\"GtkSpinButton\",\"adjustment\",\"45\",None,None,None,None,None),\n\t(1,44,\"GtkSpinButton\",\"climb-rate\",\"0.5\",None,None,None,None,None),\n\t(1,45,\"GtkAdjustment\",\"page-increment\",\"10.0\",None,None,None,None,None),\n\t(1,45,\"GtkAdjustment\",\"step-increment\",\"1.0\",None,None,None,None,None),\n\t(1,45,\"GtkAdjustment\",\"upper\",\"2147483647.0\",None,None,None,None,None),\n\t(1,46,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,46,\"GtkButton\",\"icon-name\",\"emblem-ok-symbolic\",None,None,None,None,None),\n\t(1,47,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,47,\"GtkButton\",\"icon-name\",\"object-rotate-right-symbolic\",None,None,None,None,None),\n\t(1,48,\"GtkButton\",\"has-frame\",\"False\",None,None,None,None,None),\n\t(1,48,\"GtkButton\",\"icon-name\",\"edit-redo-symbolic\",None,None,None,None,None),\n\t(1,49,\"GtkWidget\",\"halign\",\"center\",None,None,None,None,None),\n\t(1,49,\"GtkWidget\",\"valign\",\"center\",None,None,None,None,None)\n  </object_property>\n</cambalache-project>\n"
  },
  {
    "path": "src/resources/image-roll.ui",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.9.1 -->\n<interface>\n  <!-- interface-name image-roll.ui -->\n  <requires lib=\"gtk\" version=\"4.6\"/>\n  <object class=\"GtkApplicationWindow\" id=\"main_window\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkInfoBar\" id=\"error_info_bar\">\n            <property name=\"message-type\">error</property>\n            <property name=\"revealed\">False</property>\n            <property name=\"show-close-button\">True</property>\n            <child>\n              <object class=\"GtkLabel\" id=\"error_info_bar_text\">\n                <property name=\"label\">ERROR</property>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkScrolledWindow\" id=\"image_scrolled_window\">\n            <property name=\"child\">\n              <object class=\"GtkViewport\" id=\"image_viewport\">\n                <property name=\"child\">\n                  <object class=\"GtkDrawingArea\" id=\"image_widget\">\n                    <property name=\"halign\">center</property>\n                    <property name=\"valign\">center</property>\n                  </object>\n                </property>\n              </object>\n            </property>\n            <property name=\"vexpand\">True</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkBox\" id=\"action_bar\">\n            <child>\n              <object class=\"GtkButton\" id=\"previous_button\">\n                <property name=\"halign\">start</property>\n                <property name=\"has-frame\">False</property>\n                <property name=\"hexpand\">True</property>\n                <property name=\"icon-name\">go-previous-symbolic</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkFlowBox\">\n                <property name=\"column-spacing\">8</property>\n                <property name=\"halign\">center</property>\n                <property name=\"max-children-per-line\">6</property>\n                <property name=\"width-request\">300</property>\n                <child>\n                  <object class=\"GtkButton\" id=\"undo_button\">\n                    <property name=\"has-frame\">False</property>\n                    <property name=\"icon-name\">edit-undo-symbolic</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"rotate_counterclockwise_button\">\n                    <property name=\"has-frame\">False</property>\n                    <property name=\"icon-name\">object-rotate-left-symbolic</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkToggleButton\" id=\"crop_button\">\n                    <property name=\"has-frame\">False</property>\n                    <property name=\"icon-name\">crop-symbolic</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkMenuButton\" id=\"resize_button\">\n                    <property name=\"direction\">up</property>\n                    <property name=\"has-frame\">False</property>\n                    <property name=\"icon-name\">view-fullscreen-symbolic</property>\n                    <property name=\"popover\">resize_popover</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"rotate_clockwise_button\">\n                    <property name=\"has-frame\">False</property>\n                    <property name=\"icon-name\">object-rotate-right-symbolic</property>\n                  </object>\n                </child>\n                <child>\n                  <object class=\"GtkButton\" id=\"redo_button\">\n                    <property name=\"has-frame\">False</property>\n                    <property name=\"icon-name\">edit-redo-symbolic</property>\n                  </object>\n                </child>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"next_button\">\n                <property name=\"halign\">end</property>\n                <property name=\"has-frame\">False</property>\n                <property name=\"hexpand\">True</property>\n                <property name=\"icon-name\">go-next-symbolic</property>\n              </object>\n            </child>\n          </object>\n        </child>\n      </object>\n    </property>\n    <property name=\"default-height\">768</property>\n    <property name=\"default-width\">1024</property>\n    <property name=\"icon-name\">com.github.weclaw1.ImageRoll</property>\n    <property name=\"title\">Image Roll</property>\n    <child type=\"titlebar\">\n      <object class=\"GtkHeaderBar\" id=\"headerbar\">\n        <child type=\"end\">\n          <object class=\"GtkBox\">\n            <property name=\"spacing\">5</property>\n            <child>\n              <object class=\"GtkButton\" id=\"delete_button\">\n                <property name=\"icon-name\">user-trash-symbolic</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkMenuButton\" id=\"menu_button\">\n                <property name=\"direction\">none</property>\n                <property name=\"popover\">popover_menu</property>\n              </object>\n            </child>\n          </object>\n        </child>\n        <child type=\"start\">\n          <object class=\"GtkBox\">\n            <child>\n              <object class=\"GtkButton\" id=\"preview_smaller_button\">\n                <property name=\"icon-name\">zoom-out-symbolic</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkLabel\" id=\"preview_size_label\">\n                <property name=\"label\">Fit screen</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"preview_larger_button\">\n                <property name=\"icon-name\">zoom-in-symbolic</property>\n              </object>\n            </child>\n            <child>\n              <object class=\"GtkButton\" id=\"preview_fit_screen_button\">\n                <property name=\"icon-name\">zoom-fit-best-symbolic</property>\n              </object>\n            </child>\n          </object>\n        </child>\n      </object>\n    </child>\n  </object>\n  <object class=\"GtkPopoverMenu\" id=\"popover_menu\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <property name=\"orientation\">vertical</property>\n        <child>\n          <object class=\"GtkButton\" id=\"open_menu_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"label\">Open...</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"save_menu_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"label\">Save</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"save_as_menu_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"label\">Save as...</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"copy_menu_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"label\">Copy</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"set_as_wallpaper_menu_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"label\">Set as wallpaper</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"print_menu_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"label\">Print</property>\n          </object>\n        </child>\n      </object>\n    </property>\n  </object>\n  <object class=\"GtkPopover\" id=\"resize_popover\">\n    <property name=\"child\">\n      <object class=\"GtkBox\">\n        <child>\n          <object class=\"GtkToggleButton\" id=\"link_aspect_ratio_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"icon-name\">insert-link-symbolic</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSpinButton\" id=\"width_spin_button\">\n            <property name=\"adjustment\">width_adjustment</property>\n            <property name=\"climb-rate\">0.5</property>\n            <property name=\"orientation\">vertical</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkLabel\" id=\"x_label\">\n            <property name=\"label\">x</property>\n            <property name=\"margin-end\">5</property>\n            <property name=\"margin-start\">5</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkSpinButton\" id=\"height_spin_button\">\n            <property name=\"adjustment\">height_adjustment</property>\n            <property name=\"climb-rate\">0.5</property>\n            <property name=\"orientation\">vertical</property>\n          </object>\n        </child>\n        <child>\n          <object class=\"GtkButton\" id=\"apply_resize_button\">\n            <property name=\"has-frame\">False</property>\n            <property name=\"icon-name\">emblem-ok-symbolic</property>\n          </object>\n        </child>\n      </object>\n    </property>\n    <property name=\"position\">top</property>\n  </object>\n  <object class=\"GtkAdjustment\" id=\"width_adjustment\">\n    <property name=\"page-increment\">10.0</property>\n    <property name=\"step-increment\">1.0</property>\n    <property name=\"upper\">2147483647.0</property>\n  </object>\n  <object class=\"GtkAdjustment\" id=\"height_adjustment\">\n    <property name=\"page-increment\">10.0</property>\n    <property name=\"step-increment\">1.0</property>\n    <property name=\"upper\">2147483647.0</property>\n  </object>\n</interface>\n"
  },
  {
    "path": "src/resources/resources.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gresources>\n  <gresource prefix=\"/com/github/weclaw1/image-roll\">\n    <file>image-roll.ui</file>\n    <file>com.github.weclaw1.ImageRoll.svg</file>\n  </gresource>\n  <gresource prefix=\"/com/github/weclaw1/image-roll/icons/scalable/actions/\">\n    <file preprocess=\"xml-stripblanks\" alias=\"crop-symbolic.svg\">icons/crop-symbolic.svg</file>\n  </gresource>\n</gresources>\n"
  },
  {
    "path": "src/settings.rs",
    "content": "use gtk::gio;\nuse gtk::gio::prelude::SettingsExt;\nuse gtk::gio::SettingsSchemaSource;\n\nuse crate::image::PreviewSize;\n\n#[derive(Clone)]\npub struct Settings {\n    gio_settings: Option<gio::Settings>,\n    scale: PreviewSize,\n    scale_before_zoom_gesture: Option<PreviewSize>,\n    fullscreen: bool,\n}\n\nimpl Settings {\n    pub fn new(application_id: &str) -> Settings {\n        let gio_settings = match SettingsSchemaSource::default() {\n            Some(schema_source) => {\n                if schema_source.lookup(application_id, true).is_some() {\n                    Some(gio::Settings::new(application_id))\n                } else {\n                    None\n                }\n            }\n            None => None,\n        };\n\n        Settings {\n            gio_settings,\n            scale: PreviewSize::BestFit(0, 0),\n            scale_before_zoom_gesture: None,\n            fullscreen: false,\n        }\n    }\n\n    pub fn set_window_size(&self, window_size: (u32, u32)) {\n        if let Some(gio_settings) = self.gio_settings.as_ref() {\n            let (window_width, window_height) = window_size;\n            gio_settings\n                .set_uint(\"window-width\", window_width)\n                .expect(\"Could not set setting window-width.\");\n            gio_settings\n                .set_uint(\"window-height\", window_height)\n                .expect(\"Could not set setting window-height.\");\n        }\n    }\n\n    pub fn window_size(&self) -> (u32, u32) {\n        match self.gio_settings.as_ref() {\n            Some(gio_settings) => (\n                gio_settings.uint(\"window-width\"),\n                gio_settings.uint(\"window-height\"),\n            ),\n            None => (1024, 768),\n        }\n    }\n\n    pub fn set_scale(&mut self, preview_size: PreviewSize) {\n        self.scale = preview_size;\n    }\n\n    pub fn scale(&self) -> PreviewSize {\n        self.scale\n    }\n\n    pub fn set_fullscreen(&mut self, fullscreen: bool) {\n        self.fullscreen = fullscreen;\n    }\n\n    pub fn fullscreen(&self) -> bool {\n        self.fullscreen\n    }\n\n    pub fn scale_before_zoom_gesture(&self) -> Option<PreviewSize> {\n        self.scale_before_zoom_gesture\n    }\n\n    pub fn set_scale_before_zoom_gesture(\n        &mut self,\n        scale_before_zoom_gesture: Option<PreviewSize>,\n    ) {\n        self.scale_before_zoom_gesture = scale_before_zoom_gesture;\n    }\n}\n"
  },
  {
    "path": "src/test_utils.rs",
    "content": "use std::path::{Path, PathBuf};\n\npub struct TestResources {\n    file_folder: PathBuf,\n}\n\nimpl TestResources {\n    pub fn new<P: AsRef<Path>>(file_folder: P) -> Self {\n        std::fs::create_dir_all(&file_folder).unwrap();\n        Self {\n            file_folder: file_folder.as_ref().to_path_buf(),\n        }\n    }\n\n    pub fn add_file<T: AsRef<str>, C: AsRef<[u8]>>(&mut self, file_name: T, contents: C) {\n        std::fs::write(self.file_folder.join(file_name.as_ref()), contents).unwrap();\n    }\n\n    pub fn remove_file<T: AsRef<str>>(&mut self, file_name: T) {\n        std::fs::remove_file(self.file_folder.join(file_name.as_ref())).unwrap();\n    }\n\n    pub fn file_folder(&self) -> &Path {\n        self.file_folder.as_path()\n    }\n}\n\nimpl Drop for TestResources {\n    fn drop(&mut self) {\n        std::fs::remove_dir_all(self.file_folder.as_path()).unwrap();\n    }\n}\n"
  },
  {
    "path": "src/ui/action.rs",
    "content": "use std::{\n    cell::{Cell, RefCell},\n    path::PathBuf,\n    rc::Rc,\n};\n\n#[cfg(feature = \"wallpaper\")]\nuse ashpd::{\n    desktop::wallpaper::{self, SetOn},\n    WindowIdentifier,\n};\nuse gtk::{\n    gdk, gio,\n    glib::{self, timeout_future_seconds, Sender},\n    prelude::{\n        DisplayExt, FileMonitorExt, GdkCairoContextExt, GtkApplicationExt, GtkWindowExt,\n        PrintOperationExt, ToggleButtonExt, WidgetExt,\n    },\n    traits::DrawingAreaExt,\n    MessageType,\n};\n\nuse crate::{\n    file_list::FileList,\n    image::{self, CoordinatesPair, PreviewSize},\n    image_list::ImageList,\n    image_operation::{ApplyImageOperation, ImageOperation},\n    settings::Settings,\n};\n\nuse super::{\n    event::{post_event, Event},\n    widgets::Widgets,\n};\n\npub fn refresh_file_list(sender: &Sender<Event>, file_list: &mut FileList) {\n    post_event(sender, Event::HideInfoPanel);\n    if let Err(error) = file_list.refresh() {\n        post_event(\n            sender,\n            Event::DisplayMessage(error.to_string(), MessageType::Error),\n        );\n        return;\n    };\n\n    post_event(sender, Event::LoadImage(file_list.current_file_path()));\n}\n\npub fn open_file(\n    sender: &Sender<Event>,\n    image_list: Rc<RefCell<ImageList>>,\n    file_list: &mut FileList,\n    file: gio::File,\n) {\n    post_event(sender, Event::HideInfoPanel);\n    image_list.replace(ImageList::new());\n\n    let new_file_list = match FileList::new(Some(file)) {\n        Ok(file_list) => file_list,\n        Err(error) => {\n            post_event(\n                sender,\n                Event::DisplayMessage(error.to_string(), MessageType::Error),\n            );\n            return;\n        }\n    };\n\n    *file_list = new_file_list;\n\n    post_event(sender, Event::LoadImage(file_list.current_file_path()));\n\n    let sender = sender.clone();\n    file_list\n        .current_folder_monitor_mut()\n        .unwrap()\n        .connect_changed(move |_, _, _, _| {\n            post_event(&sender, Event::RefreshFileList);\n        });\n}\n\npub fn load_image(\n    sender: &Sender<Event>,\n    settings: &mut Settings,\n    widgets: &Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    file_path: Option<PathBuf>,\n) {\n    hide_info_panel(widgets);\n    let mut image_list = image_list.borrow_mut();\n    if let Some(file_path) = file_path {\n        let image = if let Some(image) = image_list.remove(&file_path) {\n            image.reload(&file_path)\n        } else {\n            image::Image::load(&file_path)\n        };\n        let image = match image {\n            Ok(image) => image,\n            Err(error) => {\n                image_list.set_current_image_path(None);\n                post_event(sender, Event::RefreshPreview(settings.scale()));\n                post_event(\n                    sender,\n                    Event::DisplayMessage(error.to_string(), MessageType::Error),\n                );\n                return;\n            }\n        };\n        image_list.insert(file_path.clone(), image);\n        widgets.window().set_title(\n            file_path\n                .file_name()\n                .and_then(|file_name| file_name.to_str()),\n        );\n        image_list.set_current_image_path(Some(file_path));\n        if let PreviewSize::BestFit(0, 0) = settings.scale() {\n            let new_scale = PreviewSize::BestFit(\n                widgets.image_viewport().allocation().width() as u32,\n                widgets.image_viewport().allocation().height() as u32,\n            );\n            settings.set_scale(new_scale);\n        }\n        post_event(sender, Event::RefreshPreview(settings.scale()));\n    } else {\n        widgets.window().set_title(Some(\"Image Roll\"));\n        image_list.set_current_image_path(None);\n        post_event(sender, Event::RefreshPreview(settings.scale()));\n    }\n}\n\npub fn next_image(\n    sender: &Sender<Event>,\n    image_list: Rc<RefCell<ImageList>>,\n    file_list: &mut FileList,\n) {\n    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {\n        current_image.remove_image_buffers();\n    }\n    file_list.next();\n    post_event(sender, Event::LoadImage(file_list.current_file_path()));\n}\n\npub fn previous_image(\n    sender: &Sender<Event>,\n    image_list: Rc<RefCell<ImageList>>,\n    file_list: &mut FileList,\n) {\n    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {\n        current_image.remove_image_buffers();\n    }\n    file_list.previous();\n    post_event(sender, Event::LoadImage(file_list.current_file_path()));\n}\n\npub fn image_viewport_resize(\n    sender: &Sender<Event>,\n    settings: &mut Settings,\n    viewport_size: (u32, u32),\n) {\n    if let PreviewSize::BestFit(_, _) = settings.scale() {\n        let new_scale = PreviewSize::BestFit(viewport_size.0, viewport_size.1);\n        settings.set_scale(new_scale);\n        post_event(sender, Event::RefreshPreview(new_scale));\n    }\n}\n\npub fn refresh_preview(\n    widgets: &Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    preview_size: PreviewSize,\n) {\n    widgets\n        .preview_size_label()\n        .set_text(String::from(preview_size).as_str());\n    if let Some(image) = image_list.borrow_mut().current_image_mut() {\n        image.create_preview_image_buffer(preview_size);\n        if let Some((preview_image_width, preview_image_height)) = image.preview_image_buffer_size()\n        {\n            widgets\n                .image_widget()\n                .set_content_width(preview_image_width as i32);\n            widgets\n                .image_widget()\n                .set_content_height(preview_image_height as i32);\n        }\n    } else {\n        widgets.image_widget().set_content_width(0);\n        widgets.image_widget().set_content_height(0);\n    }\n    widgets.image_widget().queue_draw();\n}\n\npub fn change_preview_size(\n    sender: &Sender<Event>,\n    widgets: &Widgets,\n    settings: &mut Settings,\n    mut preview_size: PreviewSize,\n) {\n    if let PreviewSize::BestFit(_, _) = preview_size {\n        let viewport_allocation = widgets.image_viewport().allocation();\n        preview_size = PreviewSize::BestFit(\n            viewport_allocation.width() as u32,\n            viewport_allocation.height() as u32,\n        );\n    }\n    settings.set_scale(preview_size);\n    post_event(sender, Event::RefreshPreview(preview_size));\n}\n\npub fn preview_smaller(sender: &Sender<Event>, settings: &Settings, value: Option<u32>) {\n    let new_scale = match value {\n        None => settings.scale().smaller(),\n        Some(value) => settings.scale().smaller_by(value),\n    };\n    if let Some(new_scale) = new_scale {\n        post_event(sender, Event::ChangePreviewSize(new_scale));\n    }\n}\n\npub fn preview_larger(sender: &Sender<Event>, settings: &Settings, value: Option<u32>) {\n    let new_scale = match value {\n        None => settings.scale().larger(),\n        Some(value) => settings.scale().larger_by(value),\n    };\n    if let Some(new_scale) = new_scale {\n        post_event(sender, Event::ChangePreviewSize(new_scale));\n    }\n}\n\npub fn preview_fit_screen(sender: &Sender<Event>) {\n    let new_scale = PreviewSize::BestFit(0, 0);\n    post_event(sender, Event::ChangePreviewSize(new_scale));\n}\n\npub fn image_edit(\n    sender: &Sender<Event>,\n    settings: &Settings,\n    image_list: Rc<RefCell<ImageList>>,\n    file_list: &FileList,\n    image_operation: ImageOperation,\n) {\n    let mut image_list = image_list.borrow_mut();\n    if let Some(mut current_image) = image_list.remove_current_image() {\n        current_image = current_image.apply_operation(&image_operation);\n        image_list.insert(file_list.current_file_path().unwrap(), current_image);\n        post_event(sender, Event::RefreshPreview(settings.scale()));\n    }\n}\n\npub fn start_selection(\n    widgets: &Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,\n    position: (u32, u32),\n) {\n    if image_list.borrow().current_image().is_some() {\n        selection_coords.replace(Some((position, position)));\n        widgets.image_widget().queue_draw();\n    }\n}\n\npub fn drag_selection(\n    widgets: &Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,\n    position: (u32, u32),\n) {\n    if let Some(((start_position_x, start_position_y), (_, _))) = selection_coords.get() {\n        if let Some(current_image) = image_list.borrow().current_image() {\n            let (position_x, position_y) = position;\n            let (image_width, image_height) = current_image.preview_image_buffer_size().unwrap();\n            if position_x >= image_width || position_y >= image_height {\n                return;\n            }\n            selection_coords.replace(Some(((start_position_x, start_position_y), position)));\n            widgets.image_widget().queue_draw();\n        }\n    }\n}\n\npub fn end_selection(\n    sender: &Sender<Event>,\n    widgets: &Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,\n) {\n    if let Some(selection_coords) = selection_coords.take() {\n        if let Some(current_image) = image_list.borrow().current_image() {\n            let crop_operation = ImageOperation::Crop(\n                current_image\n                    .preview_coords_to_image_coords(selection_coords)\n                    .unwrap(),\n            );\n            post_event(sender, Event::ImageEdit(crop_operation));\n\n            widgets.image_widget().queue_draw();\n            widgets.crop_button().set_active(false);\n        }\n    }\n}\n\npub fn resize_popover_displayed(widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {\n    if let Some(current_image) = image_list.borrow().current_image() {\n        let (image_width, image_height) = current_image.image_size().unwrap();\n        widgets.width_spin_button().set_value(image_width as f64);\n        widgets.height_spin_button().set_value(image_height as f64);\n    }\n}\n\npub fn update_resize_popover_width(widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {\n    if let Some(current_image) = image_list.borrow().current_image() {\n        let aspect_ratio = current_image.image_aspect_ratio().unwrap();\n        widgets\n            .width_spin_button()\n            .set_value(widgets.height_spin_button().value() * aspect_ratio);\n    }\n}\n\npub fn update_resize_popover_height(widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {\n    if let Some(current_image) = image_list.borrow().current_image() {\n        let aspect_ratio = current_image.image_aspect_ratio().unwrap();\n        widgets\n            .height_spin_button()\n            .set_value(widgets.width_spin_button().value() / aspect_ratio);\n    }\n}\n\npub fn save_current_image(\n    sender: &Sender<Event>,\n    image_list: Rc<RefCell<ImageList>>,\n    filename: Option<PathBuf>,\n) {\n    if let Err(error) = image_list.borrow_mut().save_current_image(filename) {\n        post_event(\n            sender,\n            Event::DisplayMessage(error.to_string(), MessageType::Error),\n        );\n    }\n}\n\npub fn delete_current_image(\n    sender: &Sender<Event>,\n    file_list: &mut FileList,\n    image_list: Rc<RefCell<ImageList>>,\n) {\n    match file_list.delete_current_file() {\n        Ok(image_path) => {\n            image_list.borrow_mut().remove(image_path.as_path());\n            post_event(\n                sender,\n                Event::DisplayMessage(\n                    format!(\n                        \"Image {} was moved to trash\",\n                        image_path\n                            .file_name()\n                            .and_then(|file_name| file_name.to_str())\n                            .unwrap_or_default()\n                    ),\n                    MessageType::Info,\n                ),\n            )\n        }\n        Err(error) => post_event(\n            sender,\n            Event::DisplayMessage(error.to_string(), MessageType::Error),\n        ),\n    }\n}\n\npub fn print(sender: &Sender<Event>, widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {\n    let print_operation = gtk::PrintOperation::new();\n\n    print_operation.connect_begin_print(move |print_operation, _| {\n        print_operation.set_n_pages(1);\n    });\n\n    let cloned_sender = sender.clone();\n    print_operation.connect_draw_page(move |_, print_context, _| {\n        if let Some(print_image_buffer) =\n            image_list\n                .borrow()\n                .current_image()\n                .and_then(|current_image| {\n                    current_image.create_print_image_buffer(\n                        print_context.width() as u32,\n                        print_context.height() as u32,\n                    )\n                })\n        {\n            let cairo_context = print_context.cairo_context();\n            cairo_context.set_source_pixbuf(\n                &print_image_buffer,\n                (print_context.width() - print_image_buffer.width() as f64) / 2.0,\n                (print_context.height() - print_image_buffer.height() as f64) / 2.0,\n            );\n            if let Err(error) = cairo_context.paint() {\n                post_event(\n                    &cloned_sender,\n                    Event::DisplayMessage(\n                        format!(\"Couldn't print current image: {}\", error),\n                        MessageType::Error,\n                    ),\n                );\n            }\n        }\n    });\n\n    print_operation.set_allow_async(true);\n    if let Err(error) = print_operation.run(\n        gtk::PrintOperationAction::PrintDialog,\n        Option::from(widgets.window()),\n    ) {\n        post_event(\n            sender,\n            Event::DisplayMessage(\n                format!(\"Couldn't print current image: {}\", error),\n                MessageType::Error,\n            ),\n        );\n    };\n}\n\npub fn undo_operation(\n    sender: &Sender<Event>,\n    settings: &Settings,\n    image_list: Rc<RefCell<ImageList>>,\n) {\n    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {\n        current_image.undo_operation();\n        post_event(sender, Event::RefreshPreview(settings.scale()));\n    }\n}\n\npub fn redo_operation(\n    sender: &Sender<Event>,\n    settings: &Settings,\n    image_list: Rc<RefCell<ImageList>>,\n) {\n    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {\n        current_image.redo_operation();\n        post_event(sender, Event::RefreshPreview(settings.scale()));\n    }\n}\n\npub fn display_message(widgets: &Widgets, message: &str, message_type: gtk::MessageType) {\n    match message_type {\n        MessageType::Error => error!(\"{}\", message),\n        MessageType::Warning => warn!(\"{}\", message),\n        MessageType::Info => info!(\"{}\", message),\n        _ => info!(\"{}\", message),\n    };\n    widgets.info_bar().set_message_type(message_type);\n    widgets.info_bar_text().set_text(message);\n    widgets.info_bar().set_revealed(true);\n    let main_context = glib::MainContext::default();\n    let info_bar = widgets.info_bar().clone();\n    main_context.spawn_local(async move {\n        timeout_future_seconds(5).await;\n        info_bar.set_revealed(false);\n    });\n}\n\npub fn hide_info_panel(widgets: &Widgets) {\n    if widgets.info_bar().message_type() != gtk::MessageType::Info\n        && widgets.info_bar().message_type() != gtk::MessageType::Warning\n    {\n        widgets.info_bar().set_revealed(false);\n    }\n}\n\npub fn toggle_fullscreen(widgets: &Widgets, settings: &mut Settings) {\n    if !settings.fullscreen() {\n        widgets.window().fullscreen();\n        settings.set_fullscreen(true);\n    } else {\n        widgets.window().unfullscreen();\n        settings.set_fullscreen(false);\n    }\n}\n\npub fn quit(application: &gtk::Application) {\n    application\n        .windows()\n        .iter()\n        .for_each(|window| window.close());\n}\n\n#[cfg(feature = \"wallpaper\")]\npub fn set_as_wallpaper(sender: &Sender<Event>, file_list: &FileList) {\n    if let Some(current_file_uri) = file_list.current_file_uri() {\n        let sender = sender.clone();\n        let main_context = glib::MainContext::default();\n        main_context.spawn_local(async move {\n            if let Err(error) = wallpaper::set_from_uri(\n                &WindowIdentifier::default(),\n                current_file_uri.as_str(),\n                true,\n                SetOn::Background,\n            )\n            .await\n            {\n                post_event(\n                    &sender,\n                    Event::DisplayMessage(error.to_string(), MessageType::Error),\n                );\n            }\n        });\n    }\n}\n#[cfg(not(feature = \"wallpaper\"))]\npub fn set_as_wallpaper(_sender: &Sender<Event>, _file_list: &FileList) {\n    error!(\"This program was built without the wallpaper feature\");\n}\n\npub fn copy_current_image(image_list: Rc<RefCell<ImageList>>) {\n    let display = gdk::Display::default().unwrap();\n    image_list.borrow().copy_current_image(display.clipboard());\n}\n\npub fn start_zoom_gesture(settings: &mut Settings) {\n    settings.set_scale_before_zoom_gesture(Some(settings.scale()));\n}\n\npub fn change_scale_on_zoom_gesture(sender: &Sender<Event>, settings: &Settings, zoom_scale: f64) {\n    if let Some(scale_before_zoom_gesture) = settings.scale_before_zoom_gesture() {\n        let new_preview_size = match scale_before_zoom_gesture {\n            PreviewSize::BestFit(_, _) | PreviewSize::OriginalSize => {\n                PreviewSize::Resized((zoom_scale * 100.0) as u32)\n            }\n            PreviewSize::Resized(old_scale) => {\n                PreviewSize::Resized((old_scale as f64 * zoom_scale) as u32)\n            }\n        };\n        post_event(sender, Event::ChangePreviewSize(new_preview_size));\n    }\n}\n\npub fn update_buttons_state(\n    widgets: &Widgets,\n    file_list: &FileList,\n    image_list: Rc<RefCell<ImageList>>,\n    settings: &Settings,\n) {\n    let previous_next_active = file_list.len() > 1;\n    widgets.next_button().set_sensitive(previous_next_active);\n    widgets\n        .previous_button()\n        .set_sensitive(previous_next_active);\n\n    let buttons_active = if let Some(current_image) = image_list.borrow().current_image() {\n        widgets\n            .undo_button()\n            .set_sensitive(current_image.can_undo_operation());\n        widgets\n            .redo_button()\n            .set_sensitive(current_image.can_redo_operation());\n        widgets\n            .save_menu_button()\n            .set_sensitive(current_image.has_operations());\n        true\n    } else {\n        widgets.undo_button().set_sensitive(false);\n        widgets.redo_button().set_sensitive(false);\n        widgets.save_menu_button().set_sensitive(false);\n        false\n    };\n\n    widgets\n        .rotate_counterclockwise_button()\n        .set_sensitive(buttons_active);\n    widgets\n        .rotate_clockwise_button()\n        .set_sensitive(buttons_active);\n    widgets.crop_button().set_sensitive(buttons_active);\n    widgets.resize_button().set_sensitive(buttons_active);\n    widgets.print_menu_button().set_sensitive(buttons_active);\n    widgets.save_as_menu_button().set_sensitive(buttons_active);\n    widgets.delete_button().set_sensitive(buttons_active);\n\n    #[cfg(feature = \"wallpaper\")]\n    widgets\n        .set_as_wallpaper_menu_button()\n        .set_sensitive(buttons_active);\n    #[cfg(not(feature = \"wallpaper\"))]\n    widgets.set_as_wallpaper_menu_button().set_sensitive(false);\n\n    widgets.copy_menu_button().set_sensitive(buttons_active);\n\n    widgets\n        .preview_smaller_button()\n        .set_sensitive(settings.scale().can_be_smaller());\n    widgets\n        .preview_larger_button()\n        .set_sensitive(settings.scale().can_be_larger());\n}\n"
  },
  {
    "path": "src/ui/controllers.rs",
    "content": "use gtk::EventControllerScrollFlags;\n\n#[derive(Clone)]\npub struct Controllers {\n    window_key_event_controller: gtk::EventControllerKey,\n    image_click_gesture: gtk::GestureClick,\n    image_motion_event_controller: gtk::EventControllerMotion,\n    image_zoom_gesture: gtk::GestureZoom,\n    image_scrolled_window_scroll_controller: gtk::EventControllerScroll,\n}\n\nimpl Controllers {\n    pub fn init() -> Self {\n        Self {\n            window_key_event_controller: gtk::EventControllerKey::new(),\n            image_click_gesture: gtk::GestureClick::new(),\n            image_motion_event_controller: gtk::EventControllerMotion::new(),\n            image_zoom_gesture: gtk::GestureZoom::new(),\n            image_scrolled_window_scroll_controller: gtk::EventControllerScroll::new(\n                EventControllerScrollFlags::BOTH_AXES,\n            ),\n        }\n    }\n\n    pub fn image_click_gesture(&self) -> &gtk::GestureClick {\n        &self.image_click_gesture\n    }\n\n    pub fn image_motion_event_controller(&self) -> &gtk::EventControllerMotion {\n        &self.image_motion_event_controller\n    }\n\n    pub fn image_zoom_gesture(&self) -> &gtk::GestureZoom {\n        &self.image_zoom_gesture\n    }\n\n    pub fn window_key_event_controller(&self) -> &gtk::EventControllerKey {\n        &self.window_key_event_controller\n    }\n\n    pub fn image_scrolled_window_scroll_controller(&self) -> &gtk::EventControllerScroll {\n        &self.image_scrolled_window_scroll_controller\n    }\n}\n"
  },
  {
    "path": "src/ui/event.rs",
    "content": "use gtk::{\n    gdk::{self, Key},\n    gdk_pixbuf::PixbufRotation,\n    gio,\n    glib::{self, timeout_future, Sender},\n    prelude::{\n        ButtonExt, DrawingAreaExtManual, FileChooserExt, FileExt, GdkCairoContextExt,\n        NativeDialogExt, PopoverExt, ToggleButtonExt, WidgetExt,\n    },\n    traits::{GestureExt, GestureSingleExt, GtkWindowExt},\n    MessageType, Window,\n};\nuse std::{\n    cell::{Cell, RefCell},\n    path::PathBuf,\n    rc::Rc,\n    time::Duration,\n};\n\nuse crate::{\n    image::{CoordinatesPair, PreviewSize},\n    image_list::ImageList,\n    image_operation::ImageOperation,\n    settings::Settings,\n};\n\nuse super::{controllers::Controllers, widgets::Widgets};\n\n#[derive(Debug)]\npub enum Event {\n    OpenFile(gio::File),\n    LoadImage(Option<PathBuf>),\n    ImageViewportResize((u32, u32)),\n    RefreshPreview(PreviewSize),\n    ChangePreviewSize(PreviewSize),\n    ImageEdit(ImageOperation),\n    StartSelection((u32, u32)),\n    DragSelection((u32, u32)),\n    SaveCurrentImage(Option<PathBuf>),\n    DeleteCurrentImage,\n    EndSelection,\n    StartZoomGesture,\n    ZoomGestureScaleChanged(f64),\n    PreviewSmaller(Option<u32>),\n    PreviewLarger(Option<u32>),\n    PreviewFitScreen,\n    NextImage,\n    PreviousImage,\n    RefreshFileList,\n    ResizePopoverDisplayed,\n    UpdateResizePopoverWidth,\n    UpdateResizePopoverHeight,\n    UndoOperation,\n    RedoOperation,\n    Print,\n    DisplayMessage(String, gtk::MessageType),\n    HideInfoPanel,\n    ToggleFullscreen,\n    CopyCurrentImage,\n    Quit,\n    SetAsWallpaper,\n}\n\npub fn post_event(sender: &glib::Sender<Event>, action: Event) {\n    if let Err(err) = sender.send(action) {\n        error!(\"Send error: {}\", err);\n    }\n}\n\npub fn connect_events(\n    widgets: Widgets,\n    sender: Sender<Event>,\n    image_list: Rc<RefCell<ImageList>>,\n    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,\n    settings: Settings,\n) {\n    connect_open_menu_button_clicked(widgets.clone(), sender.clone());\n    connect_next_button_clicked(widgets.clone(), sender.clone());\n    connect_previous_button_clicked(widgets.clone(), sender.clone());\n    connect_window_default_width_notify(widgets.clone(), settings.clone(), sender.clone());\n    connect_window_default_height_notify(widgets.clone(), settings, sender.clone());\n    connect_window_maximized_notify(widgets.clone(), sender.clone());\n    connect_window_fullscreened_notify(widgets.clone(), sender.clone());\n    connect_preview_smaller_button_clicked(widgets.clone(), sender.clone());\n    connect_preview_larger_button_clicked(widgets.clone(), sender.clone());\n    connect_preview_fit_screen_button_clicked(widgets.clone(), sender.clone());\n    connect_rotate_counterclockwise_button_clicked(widgets.clone(), sender.clone());\n    connect_rotate_clockwise_button_clicked(widgets.clone(), sender.clone());\n    connect_image_widget_draw(widgets.clone(), image_list.clone(), selection_coords);\n    connect_resize_button_activated(widgets.clone(), sender.clone());\n    connect_width_spin_button_value_changed(widgets.clone(), sender.clone());\n    connect_height_spin_button_value_changed(widgets.clone(), sender.clone());\n    connect_apply_resize_button_clicked(widgets.clone(), sender.clone());\n    connect_save_menu_button_clicked(widgets.clone(), sender.clone());\n    connect_print_menu_button_clicked(widgets.clone(), sender.clone());\n    connect_undo_button_clicked(widgets.clone(), sender.clone());\n    connect_redo_button_clicked(widgets.clone(), sender.clone());\n    connect_save_as_menu_button_clicked(widgets.clone(), image_list, sender.clone());\n    connect_delete_button_clicked(widgets.clone(), sender.clone());\n    connect_info_bar_response(widgets.clone());\n    connect_set_as_wallpaper_menu_button_clicked(widgets.clone(), sender.clone());\n    connect_copy_menu_button_clicked(widgets.clone(), sender);\n\n    widgets.window().present();\n}\n\npub fn connect_controllers(sender: Sender<Event>, widgets: Widgets, controllers: Controllers) {\n    controllers\n        .image_click_gesture()\n        .set_button(gtk::gdk::BUTTON_PRIMARY);\n    connect_controllers_to_widgets(widgets.clone(), controllers.clone());\n    connect_keybinds(controllers.clone(), widgets, sender.clone());\n    connect_image_click_pressed_gesture(controllers.clone(), sender.clone());\n    connect_image_motion_event_controller_motion(controllers.clone(), sender.clone());\n    connect_image_click_released_gesture(controllers.clone(), sender.clone());\n    connect_zoom_gesture_begin(controllers.clone(), sender.clone());\n    connect_zoom_gesture_scale_changed(controllers.clone(), sender.clone());\n    connect_image_scrolled_window_scroll_controller_scroll(controllers, sender);\n}\n\nfn connect_controllers_to_widgets(widgets: Widgets, controllers: Controllers) {\n    widgets\n        .window()\n        .add_controller(controllers.window_key_event_controller());\n    widgets\n        .image_widget()\n        .add_controller(controllers.image_click_gesture());\n    widgets\n        .image_widget()\n        .add_controller(controllers.image_motion_event_controller());\n    widgets\n        .image_widget()\n        .add_controller(controllers.image_zoom_gesture());\n    widgets\n        .image_scrolled_window()\n        .add_controller(controllers.image_scrolled_window_scroll_controller());\n}\n\npub fn connect_keybinds(controllers: Controllers, widgets: Widgets, sender: Sender<Event>) {\n    controllers\n        .window_key_event_controller()\n        .connect_key_pressed(move |_, key, _, state| {\n            match key {\n                Key::F11 => post_event(&sender, Event::ToggleFullscreen),\n                Key::Left | Key::h => {\n                    if widgets.previous_button().is_sensitive() {\n                        widgets.previous_button().emit_clicked();\n                    }\n                }\n                Key::Right | Key::l => {\n                    if widgets.next_button().is_sensitive() {\n                        widgets.next_button().emit_clicked();\n                    }\n                }\n                Key::minus | Key::KP_Subtract => {\n                    if widgets.preview_smaller_button().is_sensitive() {\n                        widgets.preview_smaller_button().emit_clicked();\n                    }\n                }\n                Key::plus | Key::KP_Add => {\n                    if widgets.preview_larger_button().is_sensitive() {\n                        widgets.preview_larger_button().emit_clicked();\n                    }\n                }\n                Key::f => {\n                    if widgets.preview_fit_screen_button().is_sensitive() {\n                        widgets.preview_fit_screen_button().emit_clicked();\n                    }\n                }\n                Key::Delete => {\n                    if widgets.delete_button().is_sensitive() {\n                        widgets.delete_button().emit_clicked();\n                    }\n                }\n                Key::S\n                    if state\n                        == (gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::CONTROL_MASK) =>\n                {\n                    if widgets.save_as_menu_button().is_sensitive() {\n                        widgets.save_as_menu_button().emit_clicked();\n                    }\n                }\n                Key::R\n                    if state\n                        == (gdk::ModifierType::SHIFT_MASK | gdk::ModifierType::CONTROL_MASK) =>\n                {\n                    if widgets.rotate_counterclockwise_button().is_sensitive() {\n                        widgets.rotate_counterclockwise_button().emit_clicked();\n                    }\n                }\n                Key::C if state == gdk::ModifierType::SHIFT_MASK => {\n                    if widgets.crop_button().is_sensitive() {\n                        widgets.crop_button().emit_clicked();\n                    }\n                }\n                Key::S if state == gdk::ModifierType::SHIFT_MASK => {\n                    if widgets.resize_button().is_sensitive() {\n                        widgets.resize_button().emit_activate();\n                    }\n                }\n                Key::q if state == gdk::ModifierType::CONTROL_MASK => {\n                    post_event(&sender, Event::Quit)\n                }\n                Key::o if state == gdk::ModifierType::CONTROL_MASK => {\n                    widgets.open_menu_button().emit_clicked();\n                }\n                Key::s if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.save_menu_button().is_sensitive() {\n                        widgets.save_menu_button().emit_clicked();\n                    }\n                }\n                Key::c if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.copy_menu_button().is_sensitive() {\n                        widgets.copy_menu_button().emit_clicked();\n                    }\n                }\n                Key::p if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.print_menu_button().is_sensitive() {\n                        widgets.print_menu_button().emit_clicked();\n                    }\n                }\n                Key::z if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.undo_button().is_sensitive() {\n                        widgets.undo_button().emit_clicked();\n                    }\n                }\n                Key::y if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.redo_button().is_sensitive() {\n                        widgets.redo_button().emit_clicked();\n                    }\n                }\n                Key::r if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.rotate_clockwise_button().is_sensitive() {\n                        widgets.rotate_clockwise_button().emit_clicked();\n                    }\n                }\n                Key::j if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.preview_smaller_button().is_sensitive() {\n                        widgets.preview_smaller_button().emit_clicked();\n                    }\n                }\n                Key::k if state == gdk::ModifierType::CONTROL_MASK => {\n                    if widgets.preview_larger_button().is_sensitive() {\n                        widgets.preview_larger_button().emit_clicked();\n                    }\n                }\n                _ => {}\n            }\n            gtk::Inhibit(false)\n        });\n}\n\nfn connect_open_menu_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .open_menu_button()\n        .connect_clicked(move |_| {\n            widgets.popover_menu().popdown();\n            let file_chooser = gtk::FileChooserNative::new(\n                Some(\"Open file\"),\n                gtk::Window::NONE,\n                gtk::FileChooserAction::Open,\n                None,\n                None,\n            );\n            file_chooser.set_transient_for(Some(widgets.window()));\n\n            let file_filter = gtk::FileFilter::new();\n            file_filter.add_mime_type(\"image/*\");\n            file_filter.set_name(Some(\"Image\"));\n\n            file_chooser.add_filter(&file_filter);\n\n            let sender = sender.clone();\n\n            file_chooser.connect_response(move |file_chooser, response| {\n                if response == gtk::ResponseType::Accept {\n                    let file = if let Some(file) = file_chooser.file() {\n                        file\n                    } else {\n                        post_event(\n                            &sender,\n                            Event::DisplayMessage(\n                                String::from(\"Couldn't load file\"),\n                                MessageType::Error,\n                            ),\n                        );\n                        return;\n                    };\n                    post_event(&sender, Event::OpenFile(file));\n                }\n                file_chooser.destroy();\n            });\n            file_chooser.show();\n            widgets.file_chooser().replace(Some(file_chooser));\n        });\n}\n\nfn connect_next_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.next_button().connect_clicked(move |_| {\n        post_event(&sender, Event::NextImage);\n    });\n}\n\nfn connect_previous_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.previous_button().connect_clicked(move |_| {\n        post_event(&sender, Event::PreviousImage);\n    });\n}\n\nfn connect_window_default_width_notify(\n    widgets: Widgets,\n    settings: Settings,\n    sender: Sender<Event>,\n) {\n    widgets\n        .clone()\n        .window()\n        .connect_default_width_notify(move |window| {\n            settings.set_window_size((window.width() as u32, window.height() as u32));\n            post_event(\n                &sender,\n                Event::ImageViewportResize((\n                    widgets.image_viewport().allocation().width() as u32,\n                    widgets.image_viewport().allocation().height() as u32,\n                )),\n            );\n        });\n}\n\nfn connect_window_default_height_notify(\n    widgets: Widgets,\n    settings: Settings,\n    sender: Sender<Event>,\n) {\n    widgets\n        .clone()\n        .window()\n        .connect_default_height_notify(move |window| {\n            settings.set_window_size((window.width() as u32, window.height() as u32));\n            post_event(\n                &sender,\n                Event::ImageViewportResize((\n                    widgets.image_viewport().allocation().width() as u32,\n                    widgets.image_viewport().allocation().height() as u32,\n                )),\n            );\n        });\n}\n\nfn connect_window_fullscreened_notify(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .window()\n        .connect_fullscreened_notify(move |_| {\n            let main_context = glib::MainContext::default();\n            let sender = sender.clone();\n            let widgets = widgets.clone();\n            main_context.spawn_local(async move {\n                timeout_future(Duration::from_millis(5)).await;\n                post_event(\n                    &sender,\n                    Event::ImageViewportResize((\n                        widgets.image_viewport().allocation().width() as u32,\n                        widgets.image_viewport().allocation().height() as u32,\n                    )),\n                );\n            });\n        });\n}\n\nfn connect_window_maximized_notify(widgets: Widgets, sender: Sender<Event>) {\n    widgets.clone().window().connect_maximized_notify(move |_| {\n        let main_context = glib::MainContext::default();\n        let sender = sender.clone();\n        let widgets = widgets.clone();\n        main_context.spawn_local(async move {\n            timeout_future(Duration::from_millis(5)).await;\n            post_event(\n                &sender,\n                Event::ImageViewportResize((\n                    widgets.image_viewport().allocation().width() as u32,\n                    widgets.image_viewport().allocation().height() as u32,\n                )),\n            );\n        });\n    });\n}\n\nfn connect_preview_smaller_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.preview_smaller_button().connect_clicked(move |_| {\n        post_event(&sender, Event::PreviewSmaller(None));\n    });\n}\n\nfn connect_preview_larger_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.preview_larger_button().connect_clicked(move |_| {\n        post_event(&sender, Event::PreviewLarger(None));\n    });\n}\n\nfn connect_preview_fit_screen_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .preview_fit_screen_button()\n        .connect_clicked(move |_| {\n            post_event(&sender, Event::PreviewFitScreen);\n        });\n}\n\nfn connect_rotate_counterclockwise_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .rotate_counterclockwise_button()\n        .connect_clicked(move |_| {\n            post_event(\n                &sender,\n                Event::ImageEdit(ImageOperation::Rotate(PixbufRotation::Counterclockwise)),\n            );\n        });\n}\n\nfn connect_rotate_clockwise_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.rotate_clockwise_button().connect_clicked(move |_| {\n        post_event(\n            &sender,\n            Event::ImageEdit(ImageOperation::Rotate(PixbufRotation::Clockwise)),\n        );\n    });\n}\n\nfn connect_image_click_pressed_gesture(controllers: Controllers, sender: Sender<Event>) {\n    controllers\n        .image_click_gesture()\n        .connect_pressed(move |_, _, x, y| {\n            post_event(&sender, Event::StartSelection((x as u32, y as u32)));\n        });\n}\n\nfn connect_image_motion_event_controller_motion(controllers: Controllers, sender: Sender<Event>) {\n    controllers\n        .image_motion_event_controller()\n        .connect_motion(move |_, x, y| {\n            post_event(&sender, Event::DragSelection((x as u32, y as u32)));\n        });\n}\n\nfn connect_image_click_released_gesture(controllers: Controllers, sender: Sender<Event>) {\n    controllers\n        .image_click_gesture()\n        .connect_released(move |_, _, _, _| {\n            post_event(&sender, Event::EndSelection);\n        });\n}\n\nfn connect_image_widget_draw(\n    widgets: Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,\n) {\n    widgets\n        .image_widget()\n        .set_draw_func(move |_, cairo_context, _, _| {\n            if let Some(current_image) = image_list.borrow().current_image() {\n                if let Some(image_buffer) = current_image.preview_image_buffer() {\n                    cairo_context.set_source_pixbuf(image_buffer, 0.0, 0.0);\n                    if let Err(error) = cairo_context.paint() {\n                        error!(\"{}\", error);\n                        return;\n                    }\n                    if let Some((\n                        (start_selection_coord_x, start_selection_coord_y),\n                        (end_selection_coord_x, end_selection_coord_y),\n                    )) = selection_coords.get()\n                    {\n                        cairo_context.set_source_rgb(0.0, 0.0, 0.0);\n                        cairo_context.set_line_width(1.0);\n                        cairo_context.rectangle(\n                            start_selection_coord_x as f64,\n                            start_selection_coord_y as f64,\n                            (end_selection_coord_x as i32 - start_selection_coord_x as i32) as f64,\n                            (end_selection_coord_y as i32 - start_selection_coord_y as i32) as f64,\n                        );\n                        if let Err(error) = cairo_context.stroke() {\n                            error!(\"{}\", error);\n                        }\n                    }\n                }\n            }\n        });\n}\n\nfn connect_resize_button_activated(widgets: Widgets, sender: Sender<Event>) {\n    widgets.resize_button().connect_activate(move |_| {\n        post_event(&sender, Event::ResizePopoverDisplayed);\n    });\n}\n\nfn connect_width_spin_button_value_changed(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .width_spin_button()\n        .connect_value_changed(move |_| {\n            if widgets.link_aspect_ratio_button().is_active() {\n                post_event(&sender, Event::UpdateResizePopoverHeight);\n            }\n        });\n}\n\nfn connect_height_spin_button_value_changed(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .height_spin_button()\n        .connect_value_changed(move |_| {\n            if widgets.link_aspect_ratio_button().is_active() {\n                post_event(&sender, Event::UpdateResizePopoverWidth);\n            }\n        });\n}\n\nfn connect_apply_resize_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .apply_resize_button()\n        .connect_clicked(move |_| {\n            post_event(\n                &sender,\n                Event::ImageEdit(ImageOperation::Resize((\n                    widgets.width_spin_button().value() as u32,\n                    widgets.height_spin_button().value() as u32,\n                ))),\n            );\n            widgets.resize_button().popdown();\n        });\n}\n\nfn connect_save_menu_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .save_menu_button()\n        .connect_clicked(move |_| {\n            widgets.popover_menu().popdown();\n            post_event(&sender, Event::SaveCurrentImage(None));\n        });\n}\n\nfn connect_save_as_menu_button_clicked(\n    widgets: Widgets,\n    image_list: Rc<RefCell<ImageList>>,\n    sender: Sender<Event>,\n) {\n    widgets\n        .clone()\n        .save_as_menu_button()\n        .connect_clicked(move |_| {\n            widgets.popover_menu().popdown();\n            let file_chooser = gtk::FileChooserNative::new(\n                Some(\"Save as...\"),\n                <Option<&Window>>::None,\n                gtk::FileChooserAction::Save,\n                None,\n                None,\n            );\n\n            file_chooser.set_transient_for(Some(widgets.window()));\n\n            if let Some(file_path) = image_list.borrow().current_image_path() {\n                if let Err(error) = file_chooser.set_file(&gio::File::for_path(file_path)) {\n                    post_event(\n                        &sender,\n                        Event::DisplayMessage(error.to_string(), MessageType::Warning),\n                    );\n                }\n            }\n\n            let file_filter = gtk::FileFilter::new();\n            file_filter.add_mime_type(\"image/*\");\n            file_filter.set_name(Some(\"Image\"));\n\n            file_chooser.add_filter(&file_filter);\n            let sender = sender.clone();\n            file_chooser.connect_response(move |file_chooser, response| {\n                if response == gtk::ResponseType::Accept {\n                    let file = if let Some(file) = file_chooser.file() {\n                        file\n                    } else {\n                        post_event(\n                            &sender,\n                            Event::DisplayMessage(\n                                String::from(\"Couldn't save file\"),\n                                MessageType::Error,\n                            ),\n                        );\n                        return;\n                    };\n                    post_event(&sender, Event::SaveCurrentImage(Some(file.path().unwrap())));\n                }\n                file_chooser.destroy();\n            });\n            file_chooser.show();\n            widgets.file_chooser().replace(Some(file_chooser));\n        });\n}\n\nfn connect_print_menu_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .print_menu_button()\n        .connect_clicked(move |_| {\n            widgets.popover_menu().popdown();\n\n            post_event(&sender, Event::Print);\n        });\n}\n\nfn connect_undo_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.undo_button().connect_clicked(move |_| {\n        post_event(&sender, Event::UndoOperation);\n    });\n}\n\nfn connect_redo_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.redo_button().connect_clicked(move |_| {\n        post_event(&sender, Event::RedoOperation);\n    });\n}\n\nfn connect_delete_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets.delete_button().connect_clicked(move |_| {\n        post_event(&sender, Event::DeleteCurrentImage);\n    });\n}\n\nfn connect_info_bar_response(widgets: Widgets) {\n    widgets.info_bar().connect_response(|info_bar, response| {\n        if response == gtk::ResponseType::Close {\n            info_bar.set_revealed(false);\n        }\n    });\n}\n\nfn connect_image_scrolled_window_scroll_controller_scroll(\n    controllers: Controllers,\n    sender: Sender<Event>,\n) {\n    controllers\n        .image_scrolled_window_scroll_controller()\n        .connect_scroll(move |_, _, y| {\n            if y < 0.0 {\n                post_event(&sender, Event::PreviewLarger(Some(5)));\n            }\n            if y > 0.0 {\n                post_event(&sender, Event::PreviewSmaller(Some(5)));\n            }\n\n            gtk::Inhibit(true)\n        });\n}\n\nfn connect_set_as_wallpaper_menu_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .set_as_wallpaper_menu_button()\n        .connect_clicked(move |_| {\n            post_event(&sender, Event::SetAsWallpaper);\n        });\n}\n\nfn connect_copy_menu_button_clicked(widgets: Widgets, sender: Sender<Event>) {\n    widgets\n        .clone()\n        .copy_menu_button()\n        .connect_clicked(move |_| {\n            widgets.popover_menu().popdown();\n            post_event(&sender, Event::CopyCurrentImage);\n        });\n}\n\nfn connect_zoom_gesture_begin(controllers: Controllers, sender: Sender<Event>) {\n    controllers.image_zoom_gesture().connect_begin(move |_, _| {\n        post_event(&sender, Event::StartZoomGesture);\n    });\n}\n\nfn connect_zoom_gesture_scale_changed(controllers: Controllers, sender: Sender<Event>) {\n    controllers\n        .image_zoom_gesture()\n        .connect_scale_changed(move |_, scale| {\n            post_event(&sender, Event::ZoomGestureScaleChanged(scale));\n        });\n}\n"
  },
  {
    "path": "src/ui/widgets.rs",
    "content": "use std::cell::RefCell;\n\nuse gtk::{\n    prelude::{GtkWindowExt, WidgetExt},\n    ApplicationWindow, Builder,\n};\n\n#[derive(Clone)]\npub struct Widgets {\n    window: ApplicationWindow,\n    open_menu_button: gtk::Button,\n    image_widget: gtk::DrawingArea,\n    popover_menu: gtk::PopoverMenu,\n    next_button: gtk::Button,\n    previous_button: gtk::Button,\n    preview_smaller_button: gtk::Button,\n    preview_larger_button: gtk::Button,\n    image_scrolled_window: gtk::ScrolledWindow,\n    image_viewport: gtk::Viewport,\n    preview_size_label: gtk::Label,\n    rotate_counterclockwise_button: gtk::Button,\n    rotate_clockwise_button: gtk::Button,\n    crop_button: gtk::ToggleButton,\n    resize_button: gtk::MenuButton,\n    width_spin_button: gtk::SpinButton,\n    height_spin_button: gtk::SpinButton,\n    link_aspect_ratio_button: gtk::ToggleButton,\n    apply_resize_button: gtk::Button,\n    info_bar: gtk::InfoBar,\n    info_bar_text: gtk::Label,\n    save_menu_button: gtk::Button,\n    print_menu_button: gtk::Button,\n    undo_button: gtk::Button,\n    redo_button: gtk::Button,\n    save_as_menu_button: gtk::Button,\n    preview_fit_screen_button: gtk::Button,\n    delete_button: gtk::Button,\n    copy_menu_button: gtk::Button,\n    set_as_wallpaper_menu_button: gtk::Button,\n    file_chooser: RefCell<Option<gtk::FileChooserNative>>,\n}\n\nimpl Widgets {\n    pub fn init(builder: Builder, application: &gtk::Application) -> Self {\n        let window: ApplicationWindow = builder\n            .object(\"main_window\")\n            .expect(\"Couldn't get main_window\");\n        window.set_application(Some(application));\n\n        let open_menu_button: gtk::Button = builder\n            .object(\"open_menu_button\")\n            .expect(\"Couldn't get open_menu_button\");\n\n        let image_widget: gtk::DrawingArea = builder\n            .object(\"image_widget\")\n            .expect(\"Couldn't get image_widget\");\n\n        let popover_menu: gtk::PopoverMenu = builder\n            .object(\"popover_menu\")\n            .expect(\"Couldn't get popover_menu\");\n\n        let next_button: gtk::Button = builder\n            .object(\"next_button\")\n            .expect(\"Couldn't get next_button\");\n        let previous_button: gtk::Button = builder\n            .object(\"previous_button\")\n            .expect(\"Couldn't get previous_button\");\n\n        let preview_smaller_button: gtk::Button = builder\n            .object(\"preview_smaller_button\")\n            .expect(\"Couldn't get preview_smaller_button\");\n        let preview_larger_button: gtk::Button = builder\n            .object(\"preview_larger_button\")\n            .expect(\"Couldn't get preview_larger_button\");\n\n        let image_scrolled_window: gtk::ScrolledWindow = builder\n            .object(\"image_scrolled_window\")\n            .expect(\"Couldn't get image_scrolled_window\");\n\n        let image_viewport: gtk::Viewport = builder\n            .object(\"image_viewport\")\n            .expect(\"Couldn't get image_viewport\");\n\n        let preview_size_label: gtk::Label = builder\n            .object(\"preview_size_label\")\n            .expect(\"Couldn't get preview_size_label\");\n\n        let rotate_counterclockwise_button: gtk::Button = builder\n            .object(\"rotate_counterclockwise_button\")\n            .expect(\"Couldn't get rotate_counterclockwise_button\");\n        let rotate_clockwise_button: gtk::Button = builder\n            .object(\"rotate_clockwise_button\")\n            .expect(\"Couldn't get rotate_clockwise_button\");\n\n        let crop_button: gtk::ToggleButton = builder\n            .object(\"crop_button\")\n            .expect(\"Couldn't get crop_button\");\n\n        let resize_button: gtk::MenuButton = builder\n            .object(\"resize_button\")\n            .expect(\"Couldn't get resize_button\");\n        resize_button.set_sensitive(false);\n\n        let width_spin_button: gtk::SpinButton = builder\n            .object(\"width_spin_button\")\n            .expect(\"Couldn't get width_spin_button\");\n        let height_spin_button: gtk::SpinButton = builder\n            .object(\"height_spin_button\")\n            .expect(\"Couldn't get height_spin_button\");\n\n        let link_aspect_ratio_button: gtk::ToggleButton = builder\n            .object(\"link_aspect_ratio_button\")\n            .expect(\"Couldn't get link_aspect_ratio_button\");\n\n        let apply_resize_button: gtk::Button = builder\n            .object(\"apply_resize_button\")\n            .expect(\"Couldn't get apply_resize_button\");\n\n        let error_info_bar: gtk::InfoBar = builder\n            .object(\"error_info_bar\")\n            .expect(\"Couldn't get error_info_bar\");\n\n        let error_info_bar_text: gtk::Label = builder\n            .object(\"error_info_bar_text\")\n            .expect(\"Couldn't get error_info_bar_text\");\n\n        let save_menu_button: gtk::Button = builder\n            .object(\"save_menu_button\")\n            .expect(\"Couldn't get save_menu_button\");\n\n        let print_menu_button: gtk::Button = builder\n            .object(\"print_menu_button\")\n            .expect(\"Couldn't get print_menu_button\");\n\n        let undo_button: gtk::Button = builder\n            .object(\"undo_button\")\n            .expect(\"Couldn't get undo_button\");\n\n        let redo_button: gtk::Button = builder\n            .object(\"redo_button\")\n            .expect(\"Couldn't get redo_button\");\n\n        let save_as_menu_button: gtk::Button = builder\n            .object(\"save_as_menu_button\")\n            .expect(\"Couldn't get save_as_menu_button\");\n\n        let preview_fit_screen_button: gtk::Button = builder\n            .object(\"preview_fit_screen_button\")\n            .expect(\"Couldn't get preview_fit_screen_button\");\n\n        let delete_button: gtk::Button = builder\n            .object(\"delete_button\")\n            .expect(\"Couldn't get delete_button\");\n\n        delete_button.add_css_class(\"destructive-action\");\n\n        let copy_menu_button: gtk::Button = builder\n            .object(\"copy_menu_button\")\n            .expect(\"Couldn't get copy_menu_button\");\n\n        let set_as_wallpaper_menu_button: gtk::Button = builder\n            .object(\"set_as_wallpaper_menu_button\")\n            .expect(\"Couldn't get set_as_wallpaper_menu_button\");\n\n        Self {\n            window,\n            open_menu_button,\n            image_widget,\n            popover_menu,\n            next_button,\n            previous_button,\n            preview_smaller_button,\n            preview_larger_button,\n            image_scrolled_window,\n            image_viewport,\n            preview_size_label,\n            rotate_counterclockwise_button,\n            rotate_clockwise_button,\n            crop_button,\n            resize_button,\n            width_spin_button,\n            height_spin_button,\n            link_aspect_ratio_button,\n            apply_resize_button,\n            info_bar: error_info_bar,\n            info_bar_text: error_info_bar_text,\n            save_menu_button,\n            print_menu_button,\n            undo_button,\n            redo_button,\n            save_as_menu_button,\n            preview_fit_screen_button,\n            delete_button,\n            copy_menu_button,\n            set_as_wallpaper_menu_button,\n            file_chooser: RefCell::new(None),\n        }\n    }\n\n    /// Get a reference to the widgets's window.\n    pub fn window(&self) -> &ApplicationWindow {\n        &self.window\n    }\n\n    /// Get a reference to the widgets's open menu button.\n    pub fn open_menu_button(&self) -> &gtk::Button {\n        &self.open_menu_button\n    }\n\n    /// Get a reference to the widgets's image widget.\n    pub fn image_widget(&self) -> &gtk::DrawingArea {\n        &self.image_widget\n    }\n\n    /// Get a reference to the widgets's popover menu.\n    pub fn popover_menu(&self) -> &gtk::PopoverMenu {\n        &self.popover_menu\n    }\n\n    /// Get a reference to the widgets's next button.\n    pub fn next_button(&self) -> &gtk::Button {\n        &self.next_button\n    }\n\n    /// Get a reference to the widgets's previous button.\n    pub fn previous_button(&self) -> &gtk::Button {\n        &self.previous_button\n    }\n\n    /// Get a reference to the widgets's preview smaller button.\n    pub fn preview_smaller_button(&self) -> &gtk::Button {\n        &self.preview_smaller_button\n    }\n\n    /// Get a reference to the widgets's preview larger button.\n    pub fn preview_larger_button(&self) -> &gtk::Button {\n        &self.preview_larger_button\n    }\n\n    /// Get a reference to the widgets's image viewport.\n    pub fn image_viewport(&self) -> &gtk::Viewport {\n        &self.image_viewport\n    }\n\n    /// Get a reference to the widgets's rotate counterclockwise button.\n    pub fn rotate_counterclockwise_button(&self) -> &gtk::Button {\n        &self.rotate_counterclockwise_button\n    }\n\n    /// Get a reference to the widgets's rotate clockwise button.\n    pub fn rotate_clockwise_button(&self) -> &gtk::Button {\n        &self.rotate_clockwise_button\n    }\n\n    /// Get a reference to the widgets's crop button.\n    pub fn crop_button(&self) -> &gtk::ToggleButton {\n        &self.crop_button\n    }\n\n    /// Get a reference to the widgets's resize button.\n    pub fn resize_button(&self) -> &gtk::MenuButton {\n        &self.resize_button\n    }\n\n    /// Get a reference to the widgets's width spin button.\n    pub fn width_spin_button(&self) -> &gtk::SpinButton {\n        &self.width_spin_button\n    }\n\n    /// Get a reference to the widgets's height spin button.\n    pub fn height_spin_button(&self) -> &gtk::SpinButton {\n        &self.height_spin_button\n    }\n\n    /// Get a reference to the widgets's link aspect ratio button.\n    pub fn link_aspect_ratio_button(&self) -> &gtk::ToggleButton {\n        &self.link_aspect_ratio_button\n    }\n\n    /// Get a reference to the widgets's apply resize button.\n    pub fn apply_resize_button(&self) -> &gtk::Button {\n        &self.apply_resize_button\n    }\n\n    /// Get a reference to the widgets's error info bar.\n    pub fn info_bar(&self) -> &gtk::InfoBar {\n        &self.info_bar\n    }\n\n    /// Get a reference to the widgets's error info bar text.\n    pub fn info_bar_text(&self) -> &gtk::Label {\n        &self.info_bar_text\n    }\n\n    /// Get a reference to the widgets's save menu button.\n    pub fn save_menu_button(&self) -> &gtk::Button {\n        &self.save_menu_button\n    }\n\n    /// Get a reference to the widgets's print menu button.\n    pub fn print_menu_button(&self) -> &gtk::Button {\n        &self.print_menu_button\n    }\n\n    /// Get a reference to the widgets's undo button.\n    pub fn undo_button(&self) -> &gtk::Button {\n        &self.undo_button\n    }\n\n    /// Get a reference to the widgets's redo button.\n    pub fn redo_button(&self) -> &gtk::Button {\n        &self.redo_button\n    }\n\n    /// Get a reference to the widgets's save as menu button.\n    pub fn save_as_menu_button(&self) -> &gtk::Button {\n        &self.save_as_menu_button\n    }\n\n    /// Get a reference to the widgets's preview fit screen button.\n    pub fn preview_fit_screen_button(&self) -> &gtk::Button {\n        &self.preview_fit_screen_button\n    }\n\n    /// Get a reference to the widgets's delete button.\n    pub fn delete_button(&self) -> &gtk::Button {\n        &self.delete_button\n    }\n\n    /// Get a reference to the widgets's preview size label.\n    pub fn preview_size_label(&self) -> &gtk::Label {\n        &self.preview_size_label\n    }\n\n    /// Get a reference to the widgets's image scrolled window.\n    pub fn image_scrolled_window(&self) -> &gtk::ScrolledWindow {\n        &self.image_scrolled_window\n    }\n\n    /// Get a reference to the widgets's set as wallpaper menu button.\n    pub fn set_as_wallpaper_menu_button(&self) -> &gtk::Button {\n        &self.set_as_wallpaper_menu_button\n    }\n\n    /// Get a reference to the widget's copy menu button\n    pub fn copy_menu_button(&self) -> &gtk::Button {\n        &self.copy_menu_button\n    }\n\n    pub fn file_chooser(&self) -> &RefCell<Option<gtk::FileChooserNative>> {\n        &self.file_chooser\n    }\n}\n"
  },
  {
    "path": "src/ui.rs",
    "content": "pub mod action;\npub mod controllers;\npub mod event;\npub mod widgets;\n"
  }
]