Full Code of weclaw1/image-roll for AI

main 50870799d10c cached
29 files
224.8 KB
62.4k tokens
232 symbols
1 requests
Download .txt
Showing preview only (235K chars total). Download the full file or copy to clipboard to get everything.
Repository: weclaw1/image-roll
Branch: main
Commit: 50870799d10c
Files: 29
Total size: 224.8 KB

Directory structure:
gitextract_s858hyto/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── build.rs
├── debian/
│   └── postinst
└── src/
    ├── app.rs
    ├── file_list.rs
    ├── image.rs
    ├── image_list.rs
    ├── image_operation.rs
    ├── main.rs
    ├── resources/
    │   ├── cargo-sources.json
    │   ├── com.github.weclaw1.ImageRoll.desktop
    │   ├── com.github.weclaw1.ImageRoll.gschema.xml
    │   ├── com.github.weclaw1.ImageRoll.metainfo.xml
    │   ├── com.github.weclaw1.ImageRoll.yaml
    │   ├── image-roll.cmb
    │   ├── image-roll.ui
    │   ├── resources.gresource
    │   └── resources.xml
    ├── settings.rs
    ├── test_utils.rs
    ├── ui/
    │   ├── action.rs
    │   ├── controllers.rs
    │   ├── event.rs
    │   └── widgets.rs
    └── ui.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
on: [push, pull_request]

name: Continuous integration

jobs:
  check:
    name: Check
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout sources
        uses: actions/checkout@v2

      - name: Install stable toolchain
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
          override: true

      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt install -y libgtk-4-dev

      - name: Set up cache
        uses: actions/cache@v2
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run cargo check
        uses: actions-rs/cargo@v1
        with:
          command: check

  test:
    name: Test Suite
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout sources
        uses: actions/checkout@v2

      - name: Install stable toolchain
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
          override: true

      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt install -y libgtk-4-dev

      - name: Set up cache
        uses: actions/cache@v2
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run cargo test
        uses: actions-rs/cargo@v1
        with:
          command: test

  lints:
    name: Lints
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout sources
        uses: actions/checkout@v2

      - name: Install stable toolchain
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
          override: true
          components: rustfmt, clippy

      - name: Run cargo fmt
        uses: actions-rs/cargo@v1
        with:
          command: fmt
          args: --all -- --check

      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt install -y libgtk-4-dev

      - name: Set up cache
        uses: actions/cache@v2
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run cargo clippy
        uses: actions-rs/cargo@v1
        with:
          command: clippy
          args: -- -D warnings

  build:
    name: Build
    needs: [check, test]
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout sources
        uses: actions/checkout@v2

      - name: Install stable toolchain
        uses: actions-rs/toolchain@v1
        with:
          profile: minimal
          toolchain: stable
          override: true

      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt install -y libgtk-4-dev

      - name: Set up cache
        uses: actions/cache@v2
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Run cargo build
        uses: actions-rs/cargo@v1
        with:
          command: build
          args: --release

      - name: Upload Artifact
        uses: actions/upload-artifact@v2
        with:
          name: image-roll
          path: target/release/image-roll

      - name: Create debian package
        run: |
          cargo install cargo-deb
          cargo deb

      - name: Upload debian Artifact
        uses: actions/upload-artifact@v2
        with:
          name: image-roll-deb
          path: target/debian/image-roll*.deb


================================================
FILE: .gitignore
================================================
/target
/build-dir
src/resources/image-roll_ui.glade~
/.flatpak-builder


================================================
FILE: Cargo.toml
================================================
[package]
name = "image-roll"
version = "2.1.0"
license = "MIT"
description = "Image Roll is a simple and fast GTK image viewer with basic image manipulation tools."
homepage = "https://github.com/weclaw1/image-roll"
repository = "https://github.com/weclaw1/image-roll"
readme = "README.md"
authors = ["Robert Węcławski <r.weclawski@gmail.com>"]
edition = "2021"

[dependencies]
log = "0.4.17"
env_logger = "0.9.0"
anyhow = "1.0.58"
ashpd = { version = "0.3.2", optional = true }

[dev-dependencies]
itertools = "0.10.3"
rand = "0.8.5"
infer = "0.9.0"

[dependencies.gtk]
package = "gtk4"
version = "0.4.8"
features = ["v4_4"]

[features]
default = ["wallpaper"]
wallpaper = ["ashpd"]  # set image as wallpaper

[package.metadata.deb]
license-file = ["LICENSE", "0"]
section = "graphics"
depends = "$auto"
maintainer-scripts = "debian"
assets = [
    ["target/release/image-roll", "usr/bin/", "755"],
    ["README.md", "usr/share/doc/image-roll/README", "644"],
    ["src/resources/com.github.weclaw1.ImageRoll.desktop", "usr/share/applications/", "644"],
    ["src/resources/com.github.weclaw1.ImageRoll.svg", "usr/share/icons/hicolor/scalable/apps/", "644"],
    ["src/resources/com.github.weclaw1.ImageRoll.Devel.svg", "usr/share/icons/hicolor/scalable/apps/", "644"],
    ["src/resources/com.github.weclaw1.ImageRoll-symbolic.svg", "usr/share/icons/hicolor/scalable/apps/", "644"],
    ["src/resources/com.github.weclaw1.ImageRoll.gschema.xml", "/usr/share/glib-2.0/schemas/", "644"],
]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Robert Węcławski

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================

# Image Roll
![Image Roll](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/com.github.weclaw1.ImageRoll.svg)

**Image Roll** is a simple and fast GTK image viewer with basic image manipulation tools.

## Features
- Written in Rust
- uses modern GTK 4
- adaptive - can be used on desktop and mobile devices
- crop image
- rotate image
- resize image
- undo and redo image edits

![Screenshot](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/screenshot.png)

## Installation

### Requirements
If you use AUR or Flatpak you may skip this section.

For this application you are required to have at least GTK 4.4.

#### Ubuntu/Debian
```
sudo apt install libgtk-4-dev
```
#### Fedora/CentOS
```
sudo dnf install gtk4-devel glib2-devel
```

### Flatpak
Flatpak is the recommended install method.
In order to install Image Roll using Flatpak run:
```
flatpak install flathub com.github.weclaw1.ImageRoll
```

### Alpine Linux
Alpine Linux provides [image-roll](https://pkgs.alpinelinux.org/packages?name=image-roll) package.
```
apk add image-roll
```

### AUR
If you run Arch Linux, you can use one of the AUR packages.
There are 3, `image-roll`, `image-roll-bin`, and `image-roll-git`.
Replace `yay` with your AUR helper of choice.

```
yay -S image-roll
```

### Debian package
On the releases page can be found deb packages which can be used on Debian and its derivatives.

### Precompiled binaries
Ready-to-go executables can be found on the releases page.

### Cargo
To install Image Roll using cargo run the following command:
```
cargo install image-roll
```


================================================
FILE: build.rs
================================================
use std::{env, process::Command};

fn main() {
    Command::new("glib-compile-resources")
        .args(&["src/resources/resources.xml", "--sourcedir=src/resources"])
        .status()
        .unwrap();

    let python_installed = Command::new("sh")
        .args(&["-c", "command -v python3"])
        .status()
        .unwrap()
        .success();
    let pip_installed = Command::new("sh")
        .args(&["-c", "command -v pip3"])
        .status()
        .unwrap()
        .success();
    let wget_installed = Command::new("sh")
        .args(&["-c", "command -v wget"])
        .status()
        .unwrap()
        .success();

    if python_installed && pip_installed && wget_installed {
        Command::new("pip3")
            .args(&["install", "aiohttp", "toml"])
            .status()
            .unwrap();
        Command::new("wget").arg("https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py").status().unwrap();
        Command::new("python3")
            .args(&[
                "flatpak-cargo-generator.py",
                "Cargo.lock",
                "-o",
                "src/resources/cargo-sources.json",
            ])
            .status()
            .unwrap();
        Command::new("rm")
            .arg("flatpak-cargo-generator.py")
            .status()
            .unwrap();
    }

    if Ok("debug".to_owned()) == env::var("PROFILE") {
        Command::new("sh")
            .args(&["-c", "mkdir -p $HOME/.local/share/glib-2.0/schemas"])
            .status()
            .unwrap();
        Command::new("sh").args(&["-c", "install -D src/resources/com.github.weclaw1.ImageRoll.gschema.xml $HOME/.local/share/glib-2.0/schemas/"]).status().unwrap();
        Command::new("sh")
            .args(&[
                "-c",
                "glib-compile-schemas $HOME/.local/share/glib-2.0/schemas/",
            ])
            .status()
            .unwrap();
    }

    println!("cargo:rerun-if-changed=src/resources/resources.xml");
    println!("cargo:rerun-if-changed=src/resources/image-roll.ui");
    println!("cargo:rerun-if-changed=src/resources/icons/crop-symbolic.svg");
    println!("cargo:rerun-if-changed=src/resources/com.github.weclaw1.ImageRoll.svg");
    println!("cargo:rerun-if-changed=src/resources/com.github.weclaw1.ImageRoll.gschema.xml");
    println!("cargo:rerun-if-changed=Cargo.lock");
}


================================================
FILE: debian/postinst
================================================
#!/bin/sh
set -e
glib-compile-schemas /usr/share/glib-2.0/schemas
exit 0

================================================
FILE: src/app.rs
================================================
use gtk::{
    gdk::Display,
    gio,
    glib::{self, timeout_future},
    prelude::*,
    Builder,
};

use std::{
    cell::{Cell, RefCell},
    rc::Rc,
    time::Duration,
};

use crate::image_list::ImageList;
use crate::settings::Settings;
use crate::ui::{
    event::{post_event, Event},
    widgets::Widgets,
};
use crate::{file_list::FileList, ui::controllers::Controllers};
use crate::{
    image::CoordinatesPair,
    ui::{action, event},
};

pub struct App {
    application: gtk::Application,
    controllers: Controllers,
    widgets: Widgets,
    image_list: Rc<RefCell<ImageList>>,
    file_list: FileList,
    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,
    settings: Settings,
    sender: glib::Sender<Event>,
}

impl App {
    pub fn create(application: &gtk::Application, file: Option<&gio::File>) {
        let bytes = glib::Bytes::from_static(include_bytes!("resources/resources.gresource"));
        let resources = gio::Resource::from_data(&bytes).expect("Couldn't load resources");
        gio::resources_register(&resources);

        let builder = Builder::from_resource("/com/github/weclaw1/image-roll/image-roll.ui");

        let widgets: Widgets = Widgets::init(builder, application);

        let controllers = Controllers::init();

        gtk::IconTheme::for_display(&Display::default().unwrap())
            .add_resource_path("/com/github/weclaw1/image-roll/icons/");

        let image_list: Rc<RefCell<ImageList>> = Rc::new(RefCell::new(ImageList::new()));

        let file_list: FileList = FileList::new(None).unwrap();

        let selection_coords: Rc<Cell<Option<CoordinatesPair>>> = Rc::new(Cell::new(None));

        let settings: Settings = Settings::new(application.application_id().unwrap().as_str());

        let (window_width, window_height) = settings.window_size();
        widgets
            .window()
            .set_default_size(window_width as i32, window_height as i32);

        let (sender, receiver) = glib::MainContext::channel::<Event>(glib::PRIORITY_DEFAULT);

        if let Some(file) = file {
            let main_context = glib::MainContext::default();
            let second_sender = sender.clone();
            let file = file.clone();
            main_context.spawn_local(async move {
                timeout_future(Duration::from_millis(10)).await;
                post_event(&second_sender, Event::OpenFile(file));
            });
        }

        let mut app = Self {
            application: application.clone(),
            controllers,
            widgets,
            image_list,
            file_list,
            selection_coords,
            settings,
            sender,
        };

        event::connect_events(
            app.widgets.clone(),
            app.sender.clone(),
            app.image_list.clone(),
            app.selection_coords.clone(),
            app.settings.clone(),
        );

        event::connect_controllers(
            app.sender.clone(),
            app.widgets.clone(),
            app.controllers.clone(),
        );

        action::update_buttons_state(
            &app.widgets,
            &app.file_list,
            app.image_list.clone(),
            &app.settings,
        );

        receiver.attach(None, move |e| {
            app.process_event(e);
            glib::Continue(true)
        });
    }

    pub fn process_event(&mut self, event: Event) {
        match event {
            Event::OpenFile(file) => action::open_file(
                &self.sender,
                self.image_list.clone(),
                &mut self.file_list,
                file,
            ),
            Event::LoadImage(file_path) => action::load_image(
                &self.sender,
                &mut self.settings,
                &self.widgets,
                self.image_list.clone(),
                file_path,
            ),
            Event::DisplayMessage(message, message_type) => {
                action::display_message(&self.widgets, message.as_str(), message_type)
            }
            Event::ImageViewportResize(viewport_size) => {
                action::image_viewport_resize(&self.sender, &mut self.settings, viewport_size)
            }
            Event::RefreshPreview(preview_size) => {
                action::refresh_preview(&self.widgets, self.image_list.clone(), preview_size)
            }
            Event::ChangePreviewSize(preview_size) => action::change_preview_size(
                &self.sender,
                &self.widgets,
                &mut self.settings,
                preview_size,
            ),
            Event::ImageEdit(image_operation) => action::image_edit(
                &self.sender,
                &self.settings,
                self.image_list.clone(),
                &self.file_list,
                image_operation,
            ),
            Event::StartSelection(position) if self.widgets.crop_button().is_active() => {
                action::start_selection(
                    &self.widgets,
                    self.image_list.clone(),
                    self.selection_coords.clone(),
                    position,
                )
            }
            Event::DragSelection(position) if self.widgets.crop_button().is_active() => {
                action::drag_selection(
                    &self.widgets,
                    self.image_list.clone(),
                    self.selection_coords.clone(),
                    position,
                )
            }
            Event::SaveCurrentImage(filename) => {
                action::save_current_image(&self.sender, self.image_list.clone(), filename);
                if self.file_list.current_folder_monitor_mut().is_none() {
                    action::refresh_file_list(&self.sender, &mut self.file_list);
                }
            }
            Event::DeleteCurrentImage => {
                action::delete_current_image(
                    &self.sender,
                    &mut self.file_list,
                    self.image_list.clone(),
                );
                if self.file_list.current_folder_monitor_mut().is_none() {
                    action::refresh_file_list(&self.sender, &mut self.file_list);
                }
            }
            Event::EndSelection if self.widgets.crop_button().is_active() => action::end_selection(
                &self.sender,
                &self.widgets,
                self.image_list.clone(),
                self.selection_coords.clone(),
            ),
            Event::PreviewSmaller(value) => {
                action::preview_smaller(&self.sender, &self.settings, value)
            }
            Event::PreviewLarger(value) => {
                action::preview_larger(&self.sender, &self.settings, value)
            }
            Event::PreviewFitScreen => action::preview_fit_screen(&self.sender),
            Event::NextImage => {
                action::next_image(&self.sender, self.image_list.clone(), &mut self.file_list)
            }
            Event::PreviousImage => {
                action::previous_image(&self.sender, self.image_list.clone(), &mut self.file_list)
            }
            Event::RefreshFileList => action::refresh_file_list(&self.sender, &mut self.file_list),
            Event::ResizePopoverDisplayed => {
                action::resize_popover_displayed(&self.widgets, self.image_list.clone())
            }
            Event::UpdateResizePopoverWidth => {
                action::update_resize_popover_width(&self.widgets, self.image_list.clone())
            }
            Event::UpdateResizePopoverHeight => {
                action::update_resize_popover_height(&self.widgets, self.image_list.clone())
            }
            Event::UndoOperation => {
                action::undo_operation(&self.sender, &self.settings, self.image_list.clone())
            }
            Event::RedoOperation => {
                action::redo_operation(&self.sender, &self.settings, self.image_list.clone())
            }
            Event::Print => action::print(&self.sender, &self.widgets, self.image_list.clone()),
            Event::HideInfoPanel => action::hide_info_panel(&self.widgets),
            Event::ToggleFullscreen => action::toggle_fullscreen(&self.widgets, &mut self.settings),
            Event::SetAsWallpaper => action::set_as_wallpaper(&self.sender, &self.file_list),
            Event::StartZoomGesture => action::start_zoom_gesture(&mut self.settings),
            Event::ZoomGestureScaleChanged(zoom_scale) => {
                action::change_scale_on_zoom_gesture(&self.sender, &self.settings, zoom_scale)
            }
            Event::CopyCurrentImage => action::copy_current_image(self.image_list.clone()),
            Event::Quit => action::quit(&self.application),
            event => debug!("Discarded unused event: {:?}", event),
        }
        action::update_buttons_state(
            &self.widgets,
            &self.file_list,
            self.image_list.clone(),
            &self.settings,
        );
    }
}


================================================
FILE: src/file_list.rs
================================================
use std::path::PathBuf;

use anyhow::{anyhow, Result};
use gtk::{
    gio::{self, Cancellable, FileMonitorFlags, FileQueryInfoFlags, FileType},
    prelude::FileExt,
};

pub struct FileList {
    file_list: Vec<gio::FileInfo>,
    current_file: Option<(usize, gio::File)>,
    current_folder: Option<gio::File>,
    current_folder_monitor: Option<gio::FileMonitor>,
}

impl FileList {
    pub fn new(current_file: Option<gio::File>) -> Result<FileList> {
        if let Some(current_file) = current_file {
            let current_folder = current_file.parent().ok_or_else(|| {
                anyhow!(
                    "Couldn't get parent folder for file: {}",
                    current_file.parse_name()
                )
            })?;
            let mut file_list: Vec<gio::FileInfo> = FileList::enumerate_files(&current_folder)?;
            file_list.sort_by_key(|file| {
                file.name()
                    .file_name()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_owned()
            });
            let current_file_index = file_list
                .iter()
                .position(|file| file.name() == current_file.basename().unwrap_or_default())
                .ok_or_else(|| {
                    anyhow!(
                        "Couldn't find {} in enumerated files",
                        current_file.parse_name()
                    )
                })?;
            let folder_monitor = current_folder
                .monitor_directory(FileMonitorFlags::NONE, <Option<&Cancellable>>::None)
                .ok();

            if folder_monitor.is_none() {
                warn!(
                    "Couldn't get monitor for directory: {}",
                    current_folder.path().unwrap().to_str().unwrap()
                );
            }

            Ok(FileList {
                file_list,
                current_file: Some((current_file_index, current_file)),
                current_folder: Some(current_folder),
                current_folder_monitor: folder_monitor,
            })
        } else {
            Ok(FileList {
                file_list: Vec::new(),
                current_file: None,
                current_folder: None,
                current_folder_monitor: None,
            })
        }
    }

    pub fn refresh(&mut self) -> Result<()> {
        if let Some(current_folder) = &self.current_folder {
            if !current_folder.query_exists(<Option<&Cancellable>>::None) {
                self.file_list = Vec::new();
                self.current_file = None;
                self.current_folder = None;
                return Ok(());
            }
            self.file_list = FileList::enumerate_files(current_folder)?;
            self.file_list.sort_by_key(|file| {
                file.name()
                    .file_name()
                    .unwrap()
                    .to_str()
                    .unwrap()
                    .to_owned()
            });

            match &self.current_file {
                Some((_, current_file)) => {
                    let file_index = self.file_list.iter().position(|file| {
                        file.name() == current_file.basename().unwrap_or_default()
                    });
                    if let Some(file_index) = file_index {
                        self.current_file = Some((file_index, self.current_file.take().unwrap().1));
                    } else {
                        self.next();
                    }
                }
                None => self.next(),
            }
        }
        Ok(())
    }

    pub fn next(&mut self) {
        if let Some(current_folder) = &self.current_folder {
            self.current_file = match self.current_file.take() {
                Some((_, _)) if self.file_list.is_empty() => None,
                Some((index, _)) if index + 1 < self.file_list.len() => Some((
                    index + 1,
                    current_folder.child(self.file_list[index + 1].name()),
                )),
                Some((index, _)) if index + 1 >= self.file_list.len() => {
                    Some((0, current_folder.child(self.file_list[0].name())))
                }
                None if !self.file_list.is_empty() => {
                    Some((0, current_folder.child(self.file_list[0].name())))
                }
                _ => None,
            }
        }
    }

    pub fn previous(&mut self) {
        if let Some(current_folder) = &self.current_folder {
            self.current_file = match self.current_file.take() {
                Some((_, _)) if self.file_list.is_empty() => None,
                Some((index, _)) if index as i64 > 0 => Some((
                    index - 1,
                    current_folder.child(self.file_list[index - 1].name()),
                )),
                Some((index, _)) if index as i64 - 1 < 0 => Some((
                    self.file_list.len() - 1,
                    current_folder.child(self.file_list[self.file_list.len() - 1].name()),
                )),
                None if !self.file_list.is_empty() => {
                    Some((0, current_folder.child(self.file_list[0].name())))
                }
                _ => None,
            }
        }
    }

    // pub fn current_folder(&self) -> Option<&gio::File> {
    //     self.current_folder.as_ref()
    // }

    pub fn current_file(&self) -> Option<&gio::File> {
        self.current_file.as_ref().map(|(_, file)| file)
    }

    #[allow(dead_code)] // currently used only with feature "wallpaper"
    pub fn current_file_uri(&self) -> Option<String> {
        self.current_file
            .as_ref()
            .map(|(_, file)| file.uri().to_string())
    }

    pub fn current_file_path(&self) -> Option<PathBuf> {
        self.current_file.as_ref().and_then(|(_, file)| file.path())
    }

    pub fn len(&self) -> usize {
        self.file_list.len()
    }

    fn enumerate_files(folder: &gio::File) -> Result<Vec<gio::FileInfo>> {
        Ok(folder
            .enumerate_children(
                "standard::*",
                FileQueryInfoFlags::NONE,
                <Option<&Cancellable>>::None,
            )?
            .into_iter()
            .filter_map(|file| file.ok())
            .filter(|file| file.file_type() == FileType::Regular)
            .filter(|file| {
                file.content_type()
                    .filter(|content_type| content_type.to_string().starts_with("image"))
                    .is_some()
            })
            .collect())
    }

    pub fn current_folder_monitor_mut(&mut self) -> Option<&mut gio::FileMonitor> {
        self.current_folder_monitor.as_mut()
    }

    pub fn delete_current_file(&mut self) -> Result<PathBuf> {
        let deleted_file = self
            .current_file()
            .ok_or_else(|| {
                anyhow!("Cannot delete current file because file list does not have a current file")
            })?
            .to_owned();
        let deleted_file_path = deleted_file
            .path()
            .ok_or_else(|| anyhow!("Deleted file does not have a valid path"))?;
        self.next();
        deleted_file.trash(<Option<&Cancellable>>::None)?;
        Ok(deleted_file_path)
    }
}

#[cfg(test)]
mod tests {
    use itertools::Itertools;

    use rand::{distributions::Alphanumeric, Rng};

    use crate::test_utils::TestResources;

    use super::*;

    const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");

    #[test]
    fn file_list_contains_image_files() {
        let mut test_resources = TestResources::new("test/file_list_contains_image_files");
        test_resources.add_file("test.png", TEST_IMAGE);
        test_resources.add_file("tes2.png", TEST_IMAGE);

        let file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test.png"),
        )))
        .unwrap();

        assert_eq!(2, file_list.len());
    }

    #[test]
    fn file_list_does_not_contain_other_files() {
        let mut test_resources = TestResources::new("test/file_list_does_not_contain_other_files");
        test_resources.add_file("test.png", TEST_IMAGE);
        test_resources.add_file("test2.png", TEST_IMAGE);
        test_resources.add_file("test.txt", "test");

        let file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test.png"),
        )))
        .unwrap();

        assert_eq!(2, file_list.len());
    }

    #[test]
    fn file_list_contains_images_without_extension() {
        let mut test_resources =
            TestResources::new("test/file_list_contains_images_without_extension");
        test_resources.add_file("test", TEST_IMAGE);
        test_resources.add_file("test2", TEST_IMAGE);

        let file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test"),
        )))
        .unwrap();

        assert_eq!(2, file_list.len());
    }

    #[test]
    fn file_list_does_not_contain_other_files_without_extension() {
        let mut test_resources =
            TestResources::new("test/file_list_does_not_contain_other_files_without_extension");
        test_resources.add_file("test", TEST_IMAGE);
        test_resources.add_file("test2", TEST_IMAGE);
        test_resources.add_file("test", TEST_IMAGE);
        test_resources.add_file("testtxt", "test");

        let file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test"),
        )))
        .unwrap();

        assert_eq!(2, file_list.len());
    }

    #[test]
    fn file_list_is_in_alphabetical_order() {
        let mut test_resources = TestResources::new("test/file_list_is_in_alphabetical_order");

        let mut random_file_names: Vec<String> = rand::thread_rng()
            .sample_iter(Alphanumeric)
            .map(char::from)
            .chunks(10)
            .into_iter()
            .map(|chunk| chunk.collect::<String>())
            .take(100)
            .map(|name| format!("{}.{}", name, "png"))
            .collect();

        random_file_names
            .iter()
            .for_each(|file_name| test_resources.add_file(file_name, TEST_IMAGE));

        random_file_names.sort();

        let mut file_list = FileList::new(Some(gio::File::for_path(
            test_resources
                .file_folder()
                .join(random_file_names.first().unwrap()),
        )))
        .unwrap();

        assert_eq!(100, file_list.len());

        for file_name in random_file_names.iter() {
            assert_eq!(
                file_name,
                file_list
                    .current_file()
                    .unwrap()
                    .basename()
                    .unwrap()
                    .to_str()
                    .unwrap()
            );
            file_list.next();
        }
    }

    #[test]
    fn refresh_file_list_loads_new_images() {
        let mut test_resources = TestResources::new("test/refresh_file_list_loads_new_images");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test.png"),
        )))
        .unwrap();
        assert_eq!(1, file_list.len());

        test_resources.add_file("test2.png", TEST_IMAGE);
        file_list.refresh().unwrap();

        assert_eq!(2, file_list.len());
    }

    #[test]
    fn refresh_file_list_removes_deleted_images() {
        let mut test_resources =
            TestResources::new("test/refresh_file_list_removes_deleted_images");
        test_resources.add_file("test.png", TEST_IMAGE);
        test_resources.add_file("test2.png", TEST_IMAGE);

        let mut file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test.png"),
        )))
        .unwrap();
        assert_eq!(2, file_list.len());

        test_resources.remove_file("test2.png");
        file_list.refresh().unwrap();

        assert_eq!(1, file_list.len());
    }

    #[test]
    fn test_change_to_next_image() {
        let mut empty_file_list = FileList::new(None).unwrap();
        assert!(empty_file_list.current_file().is_none());
        empty_file_list.next();
        assert!(empty_file_list.current_file().is_none());

        let mut test_resources = TestResources::new("test/test_change_to_next_image");
        test_resources.add_file("test1.png", TEST_IMAGE);
        test_resources.add_file("test2.png", TEST_IMAGE);
        test_resources.add_file("test3.png", TEST_IMAGE);

        let mut file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test2.png"),
        )))
        .unwrap();

        file_list.next();
        assert_eq!(
            "test3.png",
            file_list
                .current_file()
                .unwrap()
                .basename()
                .unwrap()
                .to_str()
                .unwrap()
        );

        file_list.next();
        assert_eq!(
            "test1.png",
            file_list
                .current_file()
                .unwrap()
                .basename()
                .unwrap()
                .to_str()
                .unwrap()
        );
    }

    #[test]
    fn test_change_to_previous_image() {
        let mut empty_file_list = FileList::new(None).unwrap();
        assert!(empty_file_list.current_file().is_none());
        empty_file_list.previous();
        assert!(empty_file_list.current_file().is_none());

        let mut test_resources = TestResources::new("test/test_change_to_previous_image");
        test_resources.add_file("test1.png", TEST_IMAGE);
        test_resources.add_file("test2.png", TEST_IMAGE);
        test_resources.add_file("test3.png", TEST_IMAGE);

        let mut file_list = FileList::new(Some(gio::File::for_path(
            test_resources.file_folder().join("test2.png"),
        )))
        .unwrap();

        file_list.previous();
        assert_eq!(
            "test1.png",
            file_list
                .current_file()
                .unwrap()
                .basename()
                .unwrap()
                .to_str()
                .unwrap()
        );

        file_list.previous();
        assert_eq!(
            "test3.png",
            file_list
                .current_file()
                .unwrap()
                .basename()
                .unwrap()
                .to_str()
                .unwrap()
        );
    }

    #[test]
    fn delete_current_file_deletes_file_from_filesystem() {
        let mut test_resources =
            TestResources::new("test/delete_current_file_deletes_file_from_filesystem");
        test_resources.add_file("test.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");

        let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap();

        file_list.delete_current_file().unwrap();

        assert!(std::fs::File::open(image_path).is_err());
    }

    #[test]
    fn delete_current_file_returns_deleted_file_path() {
        let mut test_resources =
            TestResources::new("test/delete_current_file_returns_deleted_file_path");
        test_resources.add_file("test.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");

        let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap();

        let file_list_current_file_path = file_list.current_file_path().unwrap();
        let deleted_file_path = file_list.delete_current_file().unwrap();

        assert_eq!(file_list_current_file_path, deleted_file_path);
    }

    #[test]
    fn file_list_goes_to_next_file_after_removal_of_current_file() {
        let mut test_resources =
            TestResources::new("test/file_list_goes_to_next_file_after_removal_of_current_file");
        test_resources.add_file("test.png", TEST_IMAGE);
        test_resources.add_file("test2.png", TEST_IMAGE);
        test_resources.add_file("test3.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");

        let mut file_list = FileList::new(Some(gio::File::for_path(image_path.as_path()))).unwrap();

        let deleted_file_path = file_list.delete_current_file().unwrap();

        assert_ne!(deleted_file_path, file_list.current_file_path().unwrap());
    }
}


================================================
FILE: src/image.rs
================================================
use std::path::Path;

use anyhow::{anyhow, Result};
use gtk::gdk_pixbuf::{InterpType, Pixbuf};

use crate::image_operation::{ApplyImageOperation, ImageOperation};

pub type Coordinates = (u32, u32);
pub type CoordinatesPair = (Coordinates, Coordinates);

pub struct Image {
    original_image_buffer: Option<Pixbuf>,
    current_image_buffer: Option<Pixbuf>,
    preview_image_buffer: Option<Pixbuf>,
    operations: Vec<ImageOperation>,
    current_operation_index: Option<usize>,
}

impl Image {
    pub fn load<P: AsRef<Path>>(path: P) -> Result<Image> {
        let image_buffer = Pixbuf::from_file(path)?;
        Ok(Image {
            original_image_buffer: Some(image_buffer.clone()),
            current_image_buffer: Some(image_buffer),
            preview_image_buffer: None,
            operations: Vec::new(),
            current_operation_index: None,
        })
    }

    pub fn save<P: AsRef<Path>>(&mut self, path: P, clear_operations: bool) -> Result<()> {
        let current_image_buffer = self
            .current_image_buffer
            .as_mut()
            .ok_or_else(|| anyhow!("Image buffer is missing!"))?;
        let extension = path
            .as_ref()
            .extension()
            .and_then(|extension| extension.to_str())
            .ok_or_else(|| anyhow!("File path doesn't have file extension"))?;
        let lowercase_extension = extension.to_lowercase();
        let file_type = match lowercase_extension.as_str() {
            file_type @ "jpeg"
            | file_type @ "png"
            | file_type @ "tiff"
            | file_type @ "ico"
            | file_type @ "bmp" => file_type,
            "jpg" => "jpeg",
            "tif" => "tiff",
            _ => "png",
        };

        let options: &[(&str, &str)] = match file_type {
            "jpeg" => &[("quality", "100")],
            "png" => &[("compression", "9")],
            _ => &[],
        };
        current_image_buffer.savev(path.as_ref(), file_type, options)?;
        if clear_operations {
            self.original_image_buffer = Some(current_image_buffer.clone());
            self.current_operation_index = None;
            self.operations.clear();
        }

        Ok(())
    }

    pub fn reload<P: AsRef<Path>>(self, path: P) -> Result<Image> {
        let original_image_buffer = Pixbuf::from_file(path)?;
        let mut current_image_buffer = original_image_buffer.clone();
        if let Some(current_operation_index) = self.current_operation_index {
            current_image_buffer = self
                .operations
                .iter()
                .take(current_operation_index + 1)
                .fold(current_image_buffer, |image, operation| {
                    image.apply_operation(operation).unwrap_or(image)
                });
        }
        Ok(Image {
            original_image_buffer: Some(original_image_buffer),
            current_image_buffer: Some(current_image_buffer),
            preview_image_buffer: None,
            operations: self.operations,
            current_operation_index: self.current_operation_index,
        })
    }

    pub fn remove_image_buffers(&mut self) {
        self.original_image_buffer = None;
        self.current_image_buffer = None;
        self.preview_image_buffer = None;
    }

    fn image_buffer_scale_to_fit(&self, canvas_width: u32, canvas_height: u32) -> Option<Pixbuf> {
        if let Some(image_buffer) = &self.current_image_buffer {
            let image_width = image_buffer.width() as f64;
            let image_height = image_buffer.height() as f64;
            let width_ratio = canvas_width as f64 / image_width;
            let height_ratio = canvas_height as f64 / image_height;
            let scale_ratio = width_ratio.min(height_ratio);
            image_buffer.scale_simple(
                (image_width * scale_ratio) as i32,
                (image_height * scale_ratio) as i32,
                InterpType::Nearest,
            )
        } else {
            None
        }
    }

    fn image_buffer_resize(&self, scale: u32) -> Option<Pixbuf> {
        if let Some(image_buffer) = &self.current_image_buffer {
            image_buffer.scale_simple(
                (image_buffer.width() as f64 * (scale as f64 / 100.0)) as i32,
                (image_buffer.height() as f64 * (scale as f64 / 100.0)) as i32,
                InterpType::Bilinear,
            )
        } else {
            None
        }
    }

    pub fn create_preview_image_buffer(&mut self, preview_size: PreviewSize) {
        self.preview_image_buffer = match preview_size {
            PreviewSize::BestFit(canvas_width, canvas_height) => {
                self.image_buffer_scale_to_fit(canvas_width, canvas_height)
            }
            PreviewSize::OriginalSize => self.current_image_buffer.clone(),
            PreviewSize::Resized(scale) => self.image_buffer_resize(scale),
        };
    }

    pub fn create_print_image_buffer(
        &self,
        canvas_width: u32,
        canvas_height: u32,
    ) -> Option<Pixbuf> {
        if let Some((image_width, image_height)) = self.image_size() {
            if image_width > canvas_width || image_height > canvas_height {
                self.image_buffer_scale_to_fit(canvas_width, canvas_height)
            } else {
                self.current_image_buffer.clone()
            }
        } else {
            None
        }
    }

    pub fn preview_image_buffer(&self) -> Option<&Pixbuf> {
        self.preview_image_buffer.as_ref()
    }

    pub fn current_image_buffer(&self) -> Option<&Pixbuf> {
        self.current_image_buffer.as_ref()
    }

    pub fn image_size(&self) -> Option<(u32, u32)> {
        self.current_image_buffer
            .as_ref()
            .map(|image_buffer| (image_buffer.width() as u32, image_buffer.height() as u32))
    }

    pub fn image_aspect_ratio(&self) -> Option<f64> {
        self.image_size()
            .map(|(image_width, image_height)| image_width as f64 / image_height as f64)
    }

    pub fn preview_image_buffer_size(&self) -> Option<(u32, u32)> {
        self.preview_image_buffer
            .as_ref()
            .map(|image_buffer| (image_buffer.width() as u32, image_buffer.height() as u32))
    }

    pub fn preview_coords_to_image_coords(
        &self,
        coords: CoordinatesPair,
    ) -> Option<CoordinatesPair> {
        let ((start_coord_x, start_coord_y), (end_coord_x, end_coord_y)) = coords;
        if let Some((image_width, image_height)) = self.image_size() {
            if let Some((preview_width, preview_height)) = self.preview_image_buffer_size() {
                Some((
                    (
                        (start_coord_x as f64 * (image_width as f64 / preview_width as f64)) as u32,
                        (start_coord_y as f64 * (image_height as f64 / preview_height as f64))
                            as u32,
                    ),
                    (
                        (end_coord_x as f64 * (image_width as f64 / preview_width as f64)) as u32,
                        (end_coord_y as f64 * (image_height as f64 / preview_height as f64)) as u32,
                    ),
                ))
            } else {
                None
            }
        } else {
            None
        }
    }

    pub fn has_operations(&self) -> bool {
        !self.operations.is_empty() && self.current_operation_index.is_some()
    }

    pub fn can_undo_operation(&self) -> bool {
        self.current_operation_index.is_some()
    }

    pub fn undo_operation(&mut self) {
        if self.can_undo_operation() {
            self.current_operation_index = self.current_operation_index.unwrap().checked_sub(1);
            self.current_image_buffer = Some(
                self.operations
                    .iter()
                    .take(
                        self.current_operation_index
                            .map_or(0, |operation_index| operation_index + 1),
                    )
                    .fold(
                        self.original_image_buffer.clone().unwrap(),
                        |image, operation| image.apply_operation(operation).unwrap_or(image),
                    ),
            );
        }
    }

    pub fn can_redo_operation(&self) -> bool {
        match self.current_operation_index {
            Some(operation_index) => operation_index + 1 < self.operations.len(),
            None => !self.operations.is_empty(),
        }
    }

    pub fn redo_operation(&mut self) {
        if self.can_redo_operation() {
            self.current_operation_index = self
                .current_operation_index
                .map_or(Some(0), |current_operation_index| {
                    Some(current_operation_index + 1)
                });
            self.current_image_buffer = Some(
                self.operations
                    .iter()
                    .take(self.current_operation_index.unwrap() + 1)
                    .fold(
                        self.original_image_buffer.clone().unwrap(),
                        |image, operation| image.apply_operation(operation).unwrap_or(image),
                    ),
            );
        }
    }
}

impl ApplyImageOperation for Image {
    type Result = Self;

    fn apply_operation(mut self, image_operation: &ImageOperation) -> Self::Result {
        if let Some(image_buffer) = self.current_image_buffer {
            let applied_operation_image_buffer = image_buffer.apply_operation(image_operation);
            if applied_operation_image_buffer.is_some() {
                if let Some(current_operation_index) = self.current_operation_index {
                    self.operations.truncate(current_operation_index + 1);
                }
                self.operations.push(*image_operation);
                self.current_operation_index = Some(self.operations.len() - 1);
            }
            self.current_image_buffer =
                Some(applied_operation_image_buffer.unwrap_or(image_buffer));
        }
        self
    }
}

#[derive(Clone, Copy, Debug)]
pub enum PreviewSize {
    BestFit(u32, u32),
    OriginalSize,
    Resized(u32),
}

impl From<PreviewSize> for String {
    fn from(value: PreviewSize) -> Self {
        match value {
            PreviewSize::BestFit(_, _) => String::from("Fit screen"),
            PreviewSize::OriginalSize => String::from("100%"),
            PreviewSize::Resized(value) => format!("{}%", value),
        }
    }
}

impl PreviewSize {
    pub fn smaller(self) -> Option<PreviewSize> {
        match self {
            PreviewSize::BestFit(_, _) => Some(PreviewSize::OriginalSize),
            PreviewSize::OriginalSize => Some(PreviewSize::Resized(75)),
            PreviewSize::Resized(value) if value > 200 => Some(PreviewSize::Resized(200)),
            PreviewSize::Resized(value) if value > 150 => Some(PreviewSize::Resized(150)),
            PreviewSize::Resized(value) if value > 133 => Some(PreviewSize::Resized(133)),
            PreviewSize::Resized(value) if value > 100 => Some(PreviewSize::OriginalSize),
            PreviewSize::Resized(value) if value > 75 => Some(PreviewSize::Resized(75)),
            PreviewSize::Resized(value) if value > 66 => Some(PreviewSize::Resized(66)),
            PreviewSize::Resized(value) if value > 50 => Some(PreviewSize::Resized(50)),
            PreviewSize::Resized(value) if value > 33 => Some(PreviewSize::Resized(33)),
            PreviewSize::Resized(value) if value > 25 => Some(PreviewSize::Resized(25)),
            PreviewSize::Resized(value) if value > 10 => Some(PreviewSize::Resized(10)),
            PreviewSize::Resized(value) if value > 5 => Some(PreviewSize::Resized(5)),
            PreviewSize::Resized(_) => None,
        }
    }

    pub fn smaller_by(self, value: u32) -> Option<PreviewSize> {
        let old_value = match self {
            PreviewSize::BestFit(_, _) => return Some(PreviewSize::OriginalSize),
            PreviewSize::OriginalSize => 100,
            PreviewSize::Resized(value) => value,
        };

        old_value
            .checked_sub(value)
            .filter(|value| value >= &5)
            .map(|value| {
                if value == 100 {
                    PreviewSize::OriginalSize
                } else {
                    PreviewSize::Resized(value)
                }
            })
    }

    pub fn can_be_smaller(&self) -> bool {
        !matches!(self, PreviewSize::Resized(value) if value <= &5)
    }

    pub fn larger(self) -> Option<PreviewSize> {
        match self {
            PreviewSize::BestFit(_, _) => Some(PreviewSize::OriginalSize),
            PreviewSize::OriginalSize => Some(PreviewSize::Resized(133)),
            PreviewSize::Resized(value) if value < 10 => Some(PreviewSize::Resized(10)),
            PreviewSize::Resized(value) if value < 25 => Some(PreviewSize::Resized(25)),
            PreviewSize::Resized(value) if value < 33 => Some(PreviewSize::Resized(33)),
            PreviewSize::Resized(value) if value < 50 => Some(PreviewSize::Resized(50)),
            PreviewSize::Resized(value) if value < 66 => Some(PreviewSize::Resized(66)),
            PreviewSize::Resized(value) if value < 75 => Some(PreviewSize::Resized(75)),
            PreviewSize::Resized(value) if value < 100 => Some(PreviewSize::OriginalSize),
            PreviewSize::Resized(value) if value < 133 => Some(PreviewSize::Resized(133)),
            PreviewSize::Resized(value) if value < 150 => Some(PreviewSize::Resized(150)),
            PreviewSize::Resized(value) if value < 200 => Some(PreviewSize::Resized(200)),
            PreviewSize::Resized(value) if value < 500 => Some(PreviewSize::Resized(500)),
            PreviewSize::Resized(_) => None,
        }
    }

    pub fn larger_by(self, value: u32) -> Option<PreviewSize> {
        let old_value = match self {
            PreviewSize::BestFit(_, _) => return Some(PreviewSize::OriginalSize),
            PreviewSize::OriginalSize => 100,
            PreviewSize::Resized(value) => value,
        };

        old_value
            .checked_add(value)
            .filter(|value| value <= &500)
            .map(|value| {
                if value == 100 {
                    PreviewSize::OriginalSize
                } else {
                    PreviewSize::Resized(value)
                }
            })
    }

    pub fn can_be_larger(&self) -> bool {
        !matches!(self, PreviewSize::Resized(value) if value >= &500)
    }
}

#[cfg(test)]
mod tests {
    use gtk::gdk_pixbuf::PixbufRotation;

    use crate::test_utils::TestResources;

    use super::*;

    const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");

    #[test]
    fn test_load_image() {
        let mut test_resources = TestResources::new("test/test_load_image");
        test_resources.add_file("test.png", TEST_IMAGE);

        let image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        assert_eq!(
            Pixbuf::from_file(test_resources.file_folder().join("test.png"))
                .unwrap()
                .pixel_bytes(),
            image.original_image_buffer.unwrap().pixel_bytes()
        );
        assert_eq!(
            Pixbuf::from_file(test_resources.file_folder().join("test.png"))
                .unwrap()
                .pixel_bytes(),
            image.current_image_buffer.unwrap().pixel_bytes()
        );
        assert!(image.operations.is_empty());
    }

    #[test]
    fn save_image() {
        let mut test_resources = TestResources::new("test/save_image");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        let saved_file_path = test_resources.file_folder().join("test2.png");
        image.save(&saved_file_path, false).unwrap();
        assert!(std::fs::File::open(saved_file_path).is_ok());
    }

    #[test]
    fn test_save_image_without_clear_operations() {
        let mut test_resources =
            TestResources::new("test/test_save_image_without_clear_operations");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));
        assert!(image.has_operations());
        image
            .save(test_resources.file_folder().join("test2.png"), false)
            .unwrap();
        assert!(image.has_operations());
        assert_ne!(
            image.original_image_buffer.unwrap().pixel_bytes(),
            image.current_image_buffer.unwrap().pixel_bytes()
        )
    }

    #[test]
    fn test_save_image_with_clear_operations() {
        let mut test_resources = TestResources::new("test/test_save_image_with_clear_operations");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));
        assert!(image.has_operations());
        image
            .save(test_resources.file_folder().join("test2.png"), true)
            .unwrap();
        assert!(!image.has_operations());
        assert_eq!(
            image.original_image_buffer.unwrap().pixel_bytes(),
            image.current_image_buffer.unwrap().pixel_bytes()
        )
    }

    #[test]
    fn save_image_uses_extensions_for_file_types_supported_by_pixbuf_save() {
        let mut test_resources = TestResources::new(
            "test/save_image_uses_extensions_for_file_types_supported_by_pixbuf_save",
        );
        let file_extensions = vec!["png", "jpg", "tif", "ico", "bmp"];
        for extension in file_extensions {
            let file_name = format!("{}.{}", "test", extension);
            test_resources.add_file(&file_name, TEST_IMAGE);
            let mut image = Image::load(test_resources.file_folder().join(file_name)).unwrap();
            let saved_file_path = test_resources
                .file_folder()
                .join(format!("{}.{}", "test2", extension));
            image.save(&saved_file_path, false).unwrap();
            let saved_file_inferred_extension = infer::get_from_path(saved_file_path)
                .unwrap()
                .unwrap()
                .extension();

            assert_eq!(saved_file_inferred_extension, extension);
        }
    }

    #[test]
    fn file_extensions_jpg_and_jpeg_are_supported() {
        let mut test_resources =
            TestResources::new("test/save_file_extensions_jpg_and_jpeg_are_supported");
        test_resources.add_file("test.jpg", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.jpg")).unwrap();
        let saved_file_path = test_resources.file_folder().join("test2.jpg");
        image.save(&saved_file_path, false).unwrap();
        let saved_file_inferred_extension = infer::get_from_path(saved_file_path)
            .unwrap()
            .unwrap()
            .extension();
        assert_eq!(saved_file_inferred_extension, "jpg");

        test_resources.add_file("test.jpeg", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.jpeg")).unwrap();
        let saved_file_path = test_resources.file_folder().join("test2.jpeg");
        image.save(&saved_file_path, false).unwrap();
        let saved_file_inferred_extension = infer::get_from_path(saved_file_path)
            .unwrap()
            .unwrap()
            .extension();
        assert_eq!(saved_file_inferred_extension, "jpg");
    }

    #[test]
    fn test_image_reload() {
        let mut test_resources = TestResources::new("test/test_image_reload");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));
        let original_image_buffer = image.original_image_buffer.clone();
        let current_image_buffer = image.current_image_buffer.clone();
        image.remove_image_buffers();
        assert!(image.original_image_buffer.is_none() && image.current_image_buffer.is_none());

        image = image
            .reload(test_resources.file_folder().join("test.png"))
            .unwrap();
        assert_eq!(
            original_image_buffer.unwrap().pixel_bytes(),
            image.original_image_buffer.unwrap().pixel_bytes()
        );
        assert_eq!(
            current_image_buffer.unwrap().pixel_bytes(),
            image.current_image_buffer.unwrap().pixel_bytes()
        );
    }

    #[test]
    fn create_preview_original_size() {
        let mut test_resources = TestResources::new("test/create_preview_original_size");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image.create_preview_image_buffer(PreviewSize::OriginalSize);

        assert_eq!(
            image.current_image_buffer.unwrap().pixel_bytes(),
            image.preview_image_buffer.unwrap().pixel_bytes()
        );
    }

    #[test]
    fn create_preview_scale_to_fit() {
        let mut test_resources = TestResources::new("test/create_preview_scale_to_fit");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((1000, 500)));
        image.create_preview_image_buffer(PreviewSize::BestFit(500, 500));

        assert_eq!((500, 250), image.preview_image_buffer_size().unwrap());
    }

    #[test]
    fn create_preview_resized() {
        let mut test_resources = TestResources::new("test/create_preview_resized");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((100, 100)));
        image.create_preview_image_buffer(PreviewSize::Resized(90));

        assert_eq!((90, 90), image.preview_image_buffer_size().unwrap());
    }

    #[test]
    fn preview_coords_to_image_coords() {
        let mut test_resources = TestResources::new("test/preview_coords_to_image_coords");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((100, 100)));
        image.create_preview_image_buffer(PreviewSize::Resized(200));

        assert_eq!(
            ((10, 10), (20, 20)),
            image
                .preview_coords_to_image_coords(((20, 20), (40, 40)))
                .unwrap()
        );
    }

    #[test]
    fn undo_operation() {
        let mut test_resources = TestResources::new("test/undo_operation");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((100, 100)));
        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));

        assert!(image.can_undo_operation());
        image.undo_operation();
        assert!(
            image.can_redo_operation()
                && image.current_operation_index == Some(0)
                && image.operations.len() == 2
        );
        image.undo_operation();
        assert!(
            image.can_redo_operation()
                && image.current_operation_index == None
                && image.operations.len() == 2
        );
    }

    #[test]
    fn redo_operation() {
        let mut test_resources = TestResources::new("test/redo_operation");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((100, 100)));
        image = image.apply_operation(&ImageOperation::Rotate(PixbufRotation::Clockwise));

        assert!(!image.can_redo_operation());
        image.undo_operation();
        assert!(
            image.can_redo_operation()
                && image.current_operation_index == Some(0)
                && image.operations.len() == 2
        );
        image.redo_operation();
        assert!(
            !image.can_redo_operation()
                && image.current_operation_index == Some(1)
                && image.operations.len() == 2
        );
    }

    #[test]
    fn apply_operation() {
        let mut test_resources = TestResources::new("test/apply_operation");
        test_resources.add_file("test.png", TEST_IMAGE);

        let mut image = Image::load(test_resources.file_folder().join("test.png")).unwrap();

        assert!(image.operations.is_empty() && image.current_operation_index.is_none());
        image = image.apply_operation(&ImageOperation::Resize((100, 100)));
        assert!(image.operations.len() == 1 && image.current_operation_index == Some(0));
        assert!(
            image.original_image_buffer.unwrap().pixel_bytes()
                != image.current_image_buffer.unwrap().pixel_bytes()
        );
    }
}


================================================
FILE: src/image_list.rs
================================================
use std::{
    collections::HashMap,
    ops::{Index, IndexMut},
    path::{Path, PathBuf},
};

use crate::image::Image;

use anyhow::{anyhow, Result};
use gtk::gdk::Texture;

pub struct ImageList {
    images: HashMap<PathBuf, Image>,
    current_image_path: Option<PathBuf>,
}

impl ImageList {
    pub fn new() -> Self {
        Self {
            images: HashMap::new(),
            current_image_path: None,
        }
    }

    pub fn remove(&mut self, key: &Path) -> Option<Image> {
        self.images.remove(key)
    }

    pub fn insert(&mut self, key: PathBuf, value: Image) {
        self.images.insert(key, value);
    }

    pub fn set_current_image_path(&mut self, current_image_path: Option<PathBuf>) {
        self.current_image_path = current_image_path;
    }

    // pub fn current_image(&self) -> Option<&Image> {
    //     self.current_image_path.as_ref().map(|image_path| self.images.get(image_path)).flatten()
    // }

    pub fn remove_current_image(&mut self) -> Option<Image> {
        self.current_image_path
            .clone()
            .and_then(|image_path| self.remove(&image_path))
    }

    pub fn current_image_mut(&mut self) -> Option<&mut Image> {
        self.current_image_path
            .clone()
            .and_then(move |image_path| self.images.get_mut(&image_path))
    }

    pub fn current_image(&self) -> Option<&Image> {
        self.current_image_path
            .as_ref()
            .and_then(|image_path| self.images.get(image_path))
    }

    pub fn current_image_path(&self) -> Option<PathBuf> {
        self.current_image_path.clone()
    }

    pub fn save_current_image(&mut self, filename: Option<PathBuf>) -> Result<()> {
        let (filename, clear_operations) = if let Some(filename) = filename {
            (filename, false)
        } else {
            (
                self.current_image_path
                    .clone()
                    .ok_or_else(|| anyhow!("Current image path is not set"))?,
                true,
            )
        };

        let current_image = self
            .current_image_mut()
            .ok_or_else(|| anyhow!("Couldn't load current image"))?;

        current_image.save(filename, clear_operations)?;
        Ok(())
    }

    pub fn copy_current_image(&self, clipboard: gtk::gdk::Clipboard) {
        if let Some(current_image) = self.current_image() {
            if let Some(buffer) = current_image.current_image_buffer() {
                clipboard.set_texture(&Texture::for_pixbuf(buffer));
            }
        }
    }
}

impl Index<&PathBuf> for ImageList {
    type Output = Image;

    fn index(&self, index: &PathBuf) -> &Self::Output {
        &self.images[index]
    }
}

impl IndexMut<&PathBuf> for ImageList {
    fn index_mut(&mut self, index: &PathBuf) -> &mut Self::Output {
        self.images.get_mut(index).unwrap()
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        image_operation::{ApplyImageOperation, ImageOperation},
        test_utils::TestResources,
    };

    use super::*;

    const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");

    #[test]
    fn save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none() {
        let mut test_resources = TestResources::new("test/save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none");
        test_resources.add_file("test.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");

        let creation_date = std::fs::File::open(&image_path)
            .unwrap()
            .metadata()
            .unwrap()
            .modified()
            .unwrap();

        let image = Image::load(&image_path).unwrap();

        let mut image_list = ImageList::new();
        image_list.insert(image_path.clone(), image);
        image_list.set_current_image_path(Some(image_path.clone()));
        image_list.save_current_image(None).unwrap();

        let modification_date = std::fs::File::open(&image_path)
            .unwrap()
            .metadata()
            .unwrap()
            .modified()
            .unwrap();
        assert!(modification_date > creation_date);
    }

    #[test]
    fn save_current_image_creates_a_new_image_when_filename_is_set() {
        let mut test_resources =
            TestResources::new("test/save_current_image_creates_a_new_image_when_filename_is_set");
        test_resources.add_file("test.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");
        let image = Image::load(&image_path).unwrap();

        let mut image_list = ImageList::new();
        image_list.insert(image_path.clone(), image);
        image_list.set_current_image_path(Some(image_path.clone()));

        let new_image_path = test_resources.file_folder().join("test2.png");
        image_list
            .save_current_image(Some(new_image_path.clone()))
            .unwrap();

        assert!(std::fs::File::open(new_image_path).is_ok());
    }

    #[test]
    fn save_current_image_clears_image_operations_when_filename_is_set_to_none() {
        let mut test_resources = TestResources::new(
            "test/save_current_image_clears_image_operations_when_filename_is_set_to_none",
        );
        test_resources.add_file("test.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");

        let mut image = Image::load(&image_path).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((10, 10)));

        let mut image_list = ImageList::new();
        image_list.insert(image_path.clone(), image);
        image_list.set_current_image_path(Some(image_path.clone()));

        assert!(image_list.current_image().unwrap().has_operations());

        image_list.save_current_image(None).unwrap();

        assert!(!image_list.current_image().unwrap().has_operations());
    }

    #[test]
    fn save_current_image_does_not_clear_image_operations_when_filename_is_set() {
        let mut test_resources = TestResources::new(
            "test/save_current_image_does_not_clear_image_operations_when_filename_is_set",
        );
        test_resources.add_file("test.png", TEST_IMAGE);

        let image_path = test_resources.file_folder().join("test.png");

        let mut image = Image::load(&image_path).unwrap();
        image = image.apply_operation(&ImageOperation::Resize((10, 10)));

        let mut image_list = ImageList::new();
        image_list.insert(image_path.clone(), image);
        image_list.set_current_image_path(Some(image_path.clone()));

        assert!(image_list.current_image().unwrap().has_operations());

        image_list
            .save_current_image(Some(test_resources.file_folder().join("test2.png")))
            .unwrap();

        assert!(image_list.current_image().unwrap().has_operations());
    }
}


================================================
FILE: src/image_operation.rs
================================================
use std::cmp;

use gtk::gdk_pixbuf::{InterpType, Pixbuf, PixbufRotation};

use crate::image::CoordinatesPair;

#[derive(Copy, Clone, Debug)]
pub enum ImageOperation {
    Rotate(PixbufRotation),
    Crop(CoordinatesPair),
    Resize((u32, u32)),
}

pub trait ApplyImageOperation {
    type Result;

    fn apply_operation(self, image_operation: &ImageOperation) -> Self::Result;
}

impl ApplyImageOperation for &Pixbuf {
    type Result = Option<Pixbuf>;

    fn apply_operation(self, image_operation: &ImageOperation) -> Self::Result {
        match image_operation {
            ImageOperation::Rotate(rotation) => self.rotate_simple(*rotation),
            ImageOperation::Crop((
                (start_position_x, start_position_y),
                (end_position_x, end_position_y),
            )) => {
                let x = *cmp::min(start_position_x, end_position_x);
                let y = *cmp::min(start_position_y, end_position_y);
                let width = *cmp::max(start_position_x, end_position_x) - x;
                let height = *cmp::max(start_position_y, end_position_y) - y;
                self.new_subpixbuf(x as i32, y as i32, width as i32, height as i32)
            }
            ImageOperation::Resize((width, height)) => {
                self.scale_simple(*width as i32, *height as i32, InterpType::Bilinear)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::test_utils::TestResources;

    use super::*;

    const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");

    #[test]
    fn test_apply_rotate_image_operation_on_pixbuf() {
        let mut test_resources =
            TestResources::new("test/test_apply_rotate_image_operation_on_pixbuf");
        test_resources.add_file("test.png", TEST_IMAGE);

        let pixbuf = Pixbuf::from_file(test_resources.file_folder().join("test.png")).unwrap();
        let image_operation = ImageOperation::Rotate(PixbufRotation::Clockwise);

        assert_eq!(
            pixbuf
                .rotate_simple(PixbufRotation::Clockwise)
                .unwrap()
                .pixel_bytes(),
            pixbuf
                .apply_operation(&image_operation)
                .unwrap()
                .pixel_bytes()
        );
    }

    #[test]
    fn test_apply_crop_image_operation_on_pixbuf() {
        let mut test_resources =
            TestResources::new("test/test_apply_crop_image_operation_on_pixbuf");
        test_resources.add_file("test.png", TEST_IMAGE);

        let pixbuf = Pixbuf::from_file(test_resources.file_folder().join("test.png")).unwrap();
        let image_operation = ImageOperation::Crop(((10, 10), (20, 20)));

        assert_eq!(
            pixbuf.new_subpixbuf(10, 10, 10, 10).unwrap().pixel_bytes(),
            pixbuf
                .apply_operation(&image_operation)
                .unwrap()
                .pixel_bytes()
        );
    }

    #[test]
    fn test_apply_resize_image_operation_on_pixbuf() {
        let mut test_resources =
            TestResources::new("test/test_apply_resize_image_operation_on_pixbuf");
        test_resources.add_file("test.png", TEST_IMAGE);

        let pixbuf = Pixbuf::from_file(test_resources.file_folder().join("test.png")).unwrap();
        let image_operation = ImageOperation::Resize((10, 10));

        assert_eq!(
            pixbuf
                .scale_simple(10, 10, InterpType::Bilinear)
                .unwrap()
                .pixel_bytes(),
            pixbuf
                .apply_operation(&image_operation)
                .unwrap()
                .pixel_bytes()
        );
    }
}


================================================
FILE: src/main.rs
================================================
use app::App;
use gtk::{gio::ApplicationFlags, prelude::*, Application};

#[macro_use]
extern crate log;

mod app;
mod file_list;
mod image;
mod image_list;
mod image_operation;
mod settings;
mod ui;

#[cfg(test)]
mod test_utils;

fn main() {
    env_logger::init();

    let application = Application::new(
        Some("com.github.weclaw1.ImageRoll"),
        ApplicationFlags::HANDLES_OPEN | ApplicationFlags::NON_UNIQUE,
    );

    application.connect_activate(|app| {
        App::create(app, None);
    });

    application.connect_open(move |app, files, _| {
        App::create(app, Some(&files[0]));
    });

    application.run();
}


================================================
FILE: src/resources/cargo-sources.json
================================================
[
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/aho-corasick/aho-corasick-0.7.18.crate",
        "sha256": "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f",
        "dest": "cargo/vendor/aho-corasick-0.7.18"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f\", \"files\": {}}",
        "dest": "cargo/vendor/aho-corasick-0.7.18",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/anyhow/anyhow-1.0.58.crate",
        "sha256": "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704",
        "dest": "cargo/vendor/anyhow-1.0.58"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704\", \"files\": {}}",
        "dest": "cargo/vendor/anyhow-1.0.58",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/ashpd/ashpd-0.3.2.crate",
        "sha256": "6dcc8ed0b5211687437636d8c95f6a608f4281d142101b3b5d314b38bfadd40f",
        "dest": "cargo/vendor/ashpd-0.3.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"6dcc8ed0b5211687437636d8c95f6a608f4281d142101b3b5d314b38bfadd40f\", \"files\": {}}",
        "dest": "cargo/vendor/ashpd-0.3.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-broadcast/async-broadcast-0.3.4.crate",
        "sha256": "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b",
        "dest": "cargo/vendor/async-broadcast-0.3.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b\", \"files\": {}}",
        "dest": "cargo/vendor/async-broadcast-0.3.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-channel/async-channel-1.6.1.crate",
        "sha256": "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319",
        "dest": "cargo/vendor/async-channel-1.6.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319\", \"files\": {}}",
        "dest": "cargo/vendor/async-channel-1.6.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-executor/async-executor-1.4.1.crate",
        "sha256": "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965",
        "dest": "cargo/vendor/async-executor-1.4.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965\", \"files\": {}}",
        "dest": "cargo/vendor/async-executor-1.4.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-io/async-io-1.6.0.crate",
        "sha256": "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b",
        "dest": "cargo/vendor/async-io-1.6.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b\", \"files\": {}}",
        "dest": "cargo/vendor/async-io-1.6.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-lock/async-lock-2.4.0.crate",
        "sha256": "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b",
        "dest": "cargo/vendor/async-lock-2.4.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b\", \"files\": {}}",
        "dest": "cargo/vendor/async-lock-2.4.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-recursion/async-recursion-0.3.2.crate",
        "sha256": "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2",
        "dest": "cargo/vendor/async-recursion-0.3.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2\", \"files\": {}}",
        "dest": "cargo/vendor/async-recursion-0.3.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-task/async-task-4.1.0.crate",
        "sha256": "677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8",
        "dest": "cargo/vendor/async-task-4.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8\", \"files\": {}}",
        "dest": "cargo/vendor/async-task-4.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/async-trait/async-trait-0.1.52.crate",
        "sha256": "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3",
        "dest": "cargo/vendor/async-trait-0.1.52"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3\", \"files\": {}}",
        "dest": "cargo/vendor/async-trait-0.1.52",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/atty/atty-0.2.14.crate",
        "sha256": "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8",
        "dest": "cargo/vendor/atty-0.2.14"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8\", \"files\": {}}",
        "dest": "cargo/vendor/atty-0.2.14",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/autocfg/autocfg-1.0.1.crate",
        "sha256": "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a",
        "dest": "cargo/vendor/autocfg-1.0.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a\", \"files\": {}}",
        "dest": "cargo/vendor/autocfg-1.0.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/bitflags/bitflags-1.3.2.crate",
        "sha256": "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a",
        "dest": "cargo/vendor/bitflags-1.3.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a\", \"files\": {}}",
        "dest": "cargo/vendor/bitflags-1.3.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/byteorder/byteorder-1.4.3.crate",
        "sha256": "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610",
        "dest": "cargo/vendor/byteorder-1.4.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610\", \"files\": {}}",
        "dest": "cargo/vendor/byteorder-1.4.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cache-padded/cache-padded-1.2.0.crate",
        "sha256": "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c",
        "dest": "cargo/vendor/cache-padded-1.2.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c\", \"files\": {}}",
        "dest": "cargo/vendor/cache-padded-1.2.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cairo-rs/cairo-rs-0.15.1.crate",
        "sha256": "b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a",
        "dest": "cargo/vendor/cairo-rs-0.15.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a\", \"files\": {}}",
        "dest": "cargo/vendor/cairo-rs-0.15.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cairo-sys-rs/cairo-sys-rs-0.15.1.crate",
        "sha256": "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8",
        "dest": "cargo/vendor/cairo-sys-rs-0.15.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8\", \"files\": {}}",
        "dest": "cargo/vendor/cairo-sys-rs-0.15.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cc/cc-1.0.73.crate",
        "sha256": "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11",
        "dest": "cargo/vendor/cc-1.0.73"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11\", \"files\": {}}",
        "dest": "cargo/vendor/cc-1.0.73",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cfb/cfb-0.7.3.crate",
        "sha256": "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f",
        "dest": "cargo/vendor/cfb-0.7.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f\", \"files\": {}}",
        "dest": "cargo/vendor/cfb-0.7.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cfg-expr/cfg-expr-0.10.1.crate",
        "sha256": "295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd",
        "dest": "cargo/vendor/cfg-expr-0.10.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"295b6eb918a60a25fec0b23a5e633e74fddbaf7bb04411e65a10c366aca4b5cd\", \"files\": {}}",
        "dest": "cargo/vendor/cfg-expr-0.10.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/cfg-if/cfg-if-1.0.0.crate",
        "sha256": "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd",
        "dest": "cargo/vendor/cfg-if-1.0.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd\", \"files\": {}}",
        "dest": "cargo/vendor/cfg-if-1.0.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/concurrent-queue/concurrent-queue-1.2.2.crate",
        "sha256": "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3",
        "dest": "cargo/vendor/concurrent-queue-1.2.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3\", \"files\": {}}",
        "dest": "cargo/vendor/concurrent-queue-1.2.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/derivative/derivative-2.2.0.crate",
        "sha256": "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b",
        "dest": "cargo/vendor/derivative-2.2.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b\", \"files\": {}}",
        "dest": "cargo/vendor/derivative-2.2.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/easy-parallel/easy-parallel-3.2.0.crate",
        "sha256": "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946",
        "dest": "cargo/vendor/easy-parallel-3.2.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946\", \"files\": {}}",
        "dest": "cargo/vendor/easy-parallel-3.2.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/either/either-1.6.1.crate",
        "sha256": "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457",
        "dest": "cargo/vendor/either-1.6.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457\", \"files\": {}}",
        "dest": "cargo/vendor/either-1.6.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/enumflags2/enumflags2-0.7.3.crate",
        "sha256": "a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def",
        "dest": "cargo/vendor/enumflags2-0.7.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def\", \"files\": {}}",
        "dest": "cargo/vendor/enumflags2-0.7.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/enumflags2_derive/enumflags2_derive-0.7.3.crate",
        "sha256": "144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4",
        "dest": "cargo/vendor/enumflags2_derive-0.7.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4\", \"files\": {}}",
        "dest": "cargo/vendor/enumflags2_derive-0.7.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/env_logger/env_logger-0.9.0.crate",
        "sha256": "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3",
        "dest": "cargo/vendor/env_logger-0.9.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3\", \"files\": {}}",
        "dest": "cargo/vendor/env_logger-0.9.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/event-listener/event-listener-2.5.2.crate",
        "sha256": "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71",
        "dest": "cargo/vendor/event-listener-2.5.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71\", \"files\": {}}",
        "dest": "cargo/vendor/event-listener-2.5.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/fastrand/fastrand-1.7.0.crate",
        "sha256": "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf",
        "dest": "cargo/vendor/fastrand-1.7.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf\", \"files\": {}}",
        "dest": "cargo/vendor/fastrand-1.7.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/field-offset/field-offset-0.3.4.crate",
        "sha256": "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92",
        "dest": "cargo/vendor/field-offset-0.3.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92\", \"files\": {}}",
        "dest": "cargo/vendor/field-offset-0.3.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/fnv/fnv-1.0.7.crate",
        "sha256": "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1",
        "dest": "cargo/vendor/fnv-1.0.7"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1\", \"files\": {}}",
        "dest": "cargo/vendor/fnv-1.0.7",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures/futures-0.3.16.crate",
        "sha256": "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b",
        "dest": "cargo/vendor/futures-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b\", \"files\": {}}",
        "dest": "cargo/vendor/futures-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-channel/futures-channel-0.3.16.crate",
        "sha256": "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9",
        "dest": "cargo/vendor/futures-channel-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9\", \"files\": {}}",
        "dest": "cargo/vendor/futures-channel-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-core/futures-core-0.3.16.crate",
        "sha256": "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99",
        "dest": "cargo/vendor/futures-core-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99\", \"files\": {}}",
        "dest": "cargo/vendor/futures-core-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-executor/futures-executor-0.3.16.crate",
        "sha256": "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c",
        "dest": "cargo/vendor/futures-executor-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c\", \"files\": {}}",
        "dest": "cargo/vendor/futures-executor-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-io/futures-io-0.3.16.crate",
        "sha256": "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582",
        "dest": "cargo/vendor/futures-io-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582\", \"files\": {}}",
        "dest": "cargo/vendor/futures-io-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-lite/futures-lite-1.12.0.crate",
        "sha256": "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48",
        "dest": "cargo/vendor/futures-lite-1.12.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48\", \"files\": {}}",
        "dest": "cargo/vendor/futures-lite-1.12.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-macro/futures-macro-0.3.16.crate",
        "sha256": "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57",
        "dest": "cargo/vendor/futures-macro-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57\", \"files\": {}}",
        "dest": "cargo/vendor/futures-macro-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-sink/futures-sink-0.3.21.crate",
        "sha256": "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868",
        "dest": "cargo/vendor/futures-sink-0.3.21"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868\", \"files\": {}}",
        "dest": "cargo/vendor/futures-sink-0.3.21",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-task/futures-task-0.3.16.crate",
        "sha256": "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2",
        "dest": "cargo/vendor/futures-task-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2\", \"files\": {}}",
        "dest": "cargo/vendor/futures-task-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/futures-util/futures-util-0.3.16.crate",
        "sha256": "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78",
        "dest": "cargo/vendor/futures-util-0.3.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78\", \"files\": {}}",
        "dest": "cargo/vendor/futures-util-0.3.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gdk-pixbuf/gdk-pixbuf-0.15.4.crate",
        "sha256": "73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49",
        "dest": "cargo/vendor/gdk-pixbuf-0.15.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49\", \"files\": {}}",
        "dest": "cargo/vendor/gdk-pixbuf-0.15.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gdk-pixbuf-sys/gdk-pixbuf-sys-0.15.1.crate",
        "sha256": "413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171",
        "dest": "cargo/vendor/gdk-pixbuf-sys-0.15.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171\", \"files\": {}}",
        "dest": "cargo/vendor/gdk-pixbuf-sys-0.15.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gdk4/gdk4-0.4.8.crate",
        "sha256": "4fabb7cf843c26b085a5d68abb95d0c0bf27a9ae2eeff9c4adb503a1eb580876",
        "dest": "cargo/vendor/gdk4-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"4fabb7cf843c26b085a5d68abb95d0c0bf27a9ae2eeff9c4adb503a1eb580876\", \"files\": {}}",
        "dest": "cargo/vendor/gdk4-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gdk4-sys/gdk4-sys-0.4.8.crate",
        "sha256": "efe7dcb44f5c00aeabff3f69abfc5673de46559070f89bd3fbb7b66485d9cef2",
        "dest": "cargo/vendor/gdk4-sys-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"efe7dcb44f5c00aeabff3f69abfc5673de46559070f89bd3fbb7b66485d9cef2\", \"files\": {}}",
        "dest": "cargo/vendor/gdk4-sys-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/getrandom/getrandom-0.2.4.crate",
        "sha256": "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c",
        "dest": "cargo/vendor/getrandom-0.2.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c\", \"files\": {}}",
        "dest": "cargo/vendor/getrandom-0.2.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gio/gio-0.15.5.crate",
        "sha256": "59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf",
        "dest": "cargo/vendor/gio-0.15.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf\", \"files\": {}}",
        "dest": "cargo/vendor/gio-0.15.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gio-sys/gio-sys-0.15.5.crate",
        "sha256": "4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3",
        "dest": "cargo/vendor/gio-sys-0.15.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3\", \"files\": {}}",
        "dest": "cargo/vendor/gio-sys-0.15.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/glib/glib-0.15.5.crate",
        "sha256": "41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7",
        "dest": "cargo/vendor/glib-0.15.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7\", \"files\": {}}",
        "dest": "cargo/vendor/glib-0.15.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/glib-macros/glib-macros-0.15.3.crate",
        "sha256": "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1",
        "dest": "cargo/vendor/glib-macros-0.15.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1\", \"files\": {}}",
        "dest": "cargo/vendor/glib-macros-0.15.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/glib-sys/glib-sys-0.15.5.crate",
        "sha256": "fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c",
        "dest": "cargo/vendor/glib-sys-0.15.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c\", \"files\": {}}",
        "dest": "cargo/vendor/glib-sys-0.15.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gobject-sys/gobject-sys-0.15.5.crate",
        "sha256": "df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a",
        "dest": "cargo/vendor/gobject-sys-0.15.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a\", \"files\": {}}",
        "dest": "cargo/vendor/gobject-sys-0.15.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/graphene-rs/graphene-rs-0.15.1.crate",
        "sha256": "7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570",
        "dest": "cargo/vendor/graphene-rs-0.15.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7c54f9fbbeefdb62c99f892dfca35f83991e2cb5b46a8dc2a715e58612f85570\", \"files\": {}}",
        "dest": "cargo/vendor/graphene-rs-0.15.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/graphene-sys/graphene-sys-0.15.10.crate",
        "sha256": "fa691fc7337ba1df599afb55c3bcb85c04f1b3f17362570e9bb0ff0d1bc3028a",
        "dest": "cargo/vendor/graphene-sys-0.15.10"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fa691fc7337ba1df599afb55c3bcb85c04f1b3f17362570e9bb0ff0d1bc3028a\", \"files\": {}}",
        "dest": "cargo/vendor/graphene-sys-0.15.10",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gsk4/gsk4-0.4.8.crate",
        "sha256": "05e9020d333280b3aa38d496495bfa9b50712eebf1ad63f0ec5bcddb5eb61be4",
        "dest": "cargo/vendor/gsk4-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"05e9020d333280b3aa38d496495bfa9b50712eebf1ad63f0ec5bcddb5eb61be4\", \"files\": {}}",
        "dest": "cargo/vendor/gsk4-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gsk4-sys/gsk4-sys-0.4.8.crate",
        "sha256": "7add39ccf60078508c838643a2dcc91f045c46ed63b5ea6ab701b2e25bda3fea",
        "dest": "cargo/vendor/gsk4-sys-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7add39ccf60078508c838643a2dcc91f045c46ed63b5ea6ab701b2e25bda3fea\", \"files\": {}}",
        "dest": "cargo/vendor/gsk4-sys-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gtk4/gtk4-0.4.8.crate",
        "sha256": "c64f0c2a3d80e899dc3febddad5bac193ffcf74a0fd7e31037f30dd34d6f7396",
        "dest": "cargo/vendor/gtk4-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c64f0c2a3d80e899dc3febddad5bac193ffcf74a0fd7e31037f30dd34d6f7396\", \"files\": {}}",
        "dest": "cargo/vendor/gtk4-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gtk4-macros/gtk4-macros-0.4.8.crate",
        "sha256": "fafbcc920af4eb677d7d164853e7040b9de5a22379c596f570190c675d45f7a7",
        "dest": "cargo/vendor/gtk4-macros-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fafbcc920af4eb677d7d164853e7040b9de5a22379c596f570190c675d45f7a7\", \"files\": {}}",
        "dest": "cargo/vendor/gtk4-macros-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/gtk4-sys/gtk4-sys-0.4.8.crate",
        "sha256": "5bc8006eea634b7c72da3ff79e24606e45f21b3b832a3c5a1f543f5f97eb0f63",
        "dest": "cargo/vendor/gtk4-sys-0.4.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"5bc8006eea634b7c72da3ff79e24606e45f21b3b832a3c5a1f543f5f97eb0f63\", \"files\": {}}",
        "dest": "cargo/vendor/gtk4-sys-0.4.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/heck/heck-0.4.0.crate",
        "sha256": "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9",
        "dest": "cargo/vendor/heck-0.4.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9\", \"files\": {}}",
        "dest": "cargo/vendor/heck-0.4.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/hermit-abi/hermit-abi-0.1.19.crate",
        "sha256": "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33",
        "dest": "cargo/vendor/hermit-abi-0.1.19"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33\", \"files\": {}}",
        "dest": "cargo/vendor/hermit-abi-0.1.19",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/hex/hex-0.4.3.crate",
        "sha256": "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70",
        "dest": "cargo/vendor/hex-0.4.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70\", \"files\": {}}",
        "dest": "cargo/vendor/hex-0.4.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/humantime/humantime-2.1.0.crate",
        "sha256": "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4",
        "dest": "cargo/vendor/humantime-2.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4\", \"files\": {}}",
        "dest": "cargo/vendor/humantime-2.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/infer/infer-0.9.0.crate",
        "sha256": "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a",
        "dest": "cargo/vendor/infer-0.9.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a\", \"files\": {}}",
        "dest": "cargo/vendor/infer-0.9.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/instant/instant-0.1.12.crate",
        "sha256": "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c",
        "dest": "cargo/vendor/instant-0.1.12"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c\", \"files\": {}}",
        "dest": "cargo/vendor/instant-0.1.12",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/itertools/itertools-0.10.3.crate",
        "sha256": "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3",
        "dest": "cargo/vendor/itertools-0.10.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3\", \"files\": {}}",
        "dest": "cargo/vendor/itertools-0.10.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/lazy_static/lazy_static-1.4.0.crate",
        "sha256": "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646",
        "dest": "cargo/vendor/lazy_static-1.4.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646\", \"files\": {}}",
        "dest": "cargo/vendor/lazy_static-1.4.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/libc/libc-0.2.118.crate",
        "sha256": "06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94",
        "dest": "cargo/vendor/libc-0.2.118"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"06e509672465a0504304aa87f9f176f2b2b716ed8fb105ebe5c02dc6dce96a94\", \"files\": {}}",
        "dest": "cargo/vendor/libc-0.2.118",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/log/log-0.4.17.crate",
        "sha256": "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e",
        "dest": "cargo/vendor/log-0.4.17"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e\", \"files\": {}}",
        "dest": "cargo/vendor/log-0.4.17",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/memchr/memchr-2.4.0.crate",
        "sha256": "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc",
        "dest": "cargo/vendor/memchr-2.4.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc\", \"files\": {}}",
        "dest": "cargo/vendor/memchr-2.4.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/memoffset/memoffset-0.6.4.crate",
        "sha256": "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9",
        "dest": "cargo/vendor/memoffset-0.6.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9\", \"files\": {}}",
        "dest": "cargo/vendor/memoffset-0.6.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/nix/nix-0.23.1.crate",
        "sha256": "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6",
        "dest": "cargo/vendor/nix-0.23.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6\", \"files\": {}}",
        "dest": "cargo/vendor/nix-0.23.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/once_cell/once_cell-1.8.0.crate",
        "sha256": "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56",
        "dest": "cargo/vendor/once_cell-1.8.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56\", \"files\": {}}",
        "dest": "cargo/vendor/once_cell-1.8.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/ordered-stream/ordered-stream-0.0.1.crate",
        "sha256": "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1",
        "dest": "cargo/vendor/ordered-stream-0.0.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1\", \"files\": {}}",
        "dest": "cargo/vendor/ordered-stream-0.0.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/pango/pango-0.15.2.crate",
        "sha256": "79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94",
        "dest": "cargo/vendor/pango-0.15.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94\", \"files\": {}}",
        "dest": "cargo/vendor/pango-0.15.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/pango-sys/pango-sys-0.15.1.crate",
        "sha256": "7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2",
        "dest": "cargo/vendor/pango-sys-0.15.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2\", \"files\": {}}",
        "dest": "cargo/vendor/pango-sys-0.15.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/parking/parking-2.0.0.crate",
        "sha256": "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72",
        "dest": "cargo/vendor/parking-2.0.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72\", \"files\": {}}",
        "dest": "cargo/vendor/parking-2.0.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/pest/pest-2.1.3.crate",
        "sha256": "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53",
        "dest": "cargo/vendor/pest-2.1.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53\", \"files\": {}}",
        "dest": "cargo/vendor/pest-2.1.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/pin-project-lite/pin-project-lite-0.2.7.crate",
        "sha256": "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443",
        "dest": "cargo/vendor/pin-project-lite-0.2.7"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443\", \"files\": {}}",
        "dest": "cargo/vendor/pin-project-lite-0.2.7",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/pin-utils/pin-utils-0.1.0.crate",
        "sha256": "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184",
        "dest": "cargo/vendor/pin-utils-0.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184\", \"files\": {}}",
        "dest": "cargo/vendor/pin-utils-0.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/pkg-config/pkg-config-0.3.25.crate",
        "sha256": "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae",
        "dest": "cargo/vendor/pkg-config-0.3.25"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae\", \"files\": {}}",
        "dest": "cargo/vendor/pkg-config-0.3.25",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/polling/polling-2.2.0.crate",
        "sha256": "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259",
        "dest": "cargo/vendor/polling-2.2.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259\", \"files\": {}}",
        "dest": "cargo/vendor/polling-2.2.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/ppv-lite86/ppv-lite86-0.2.16.crate",
        "sha256": "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872",
        "dest": "cargo/vendor/ppv-lite86-0.2.16"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872\", \"files\": {}}",
        "dest": "cargo/vendor/ppv-lite86-0.2.16",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/proc-macro-crate/proc-macro-crate-1.0.0.crate",
        "sha256": "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92",
        "dest": "cargo/vendor/proc-macro-crate-1.0.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92\", \"files\": {}}",
        "dest": "cargo/vendor/proc-macro-crate-1.0.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/proc-macro-error/proc-macro-error-1.0.4.crate",
        "sha256": "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c",
        "dest": "cargo/vendor/proc-macro-error-1.0.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c\", \"files\": {}}",
        "dest": "cargo/vendor/proc-macro-error-1.0.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/proc-macro-error-attr/proc-macro-error-attr-1.0.4.crate",
        "sha256": "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869",
        "dest": "cargo/vendor/proc-macro-error-attr-1.0.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869\", \"files\": {}}",
        "dest": "cargo/vendor/proc-macro-error-attr-1.0.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/proc-macro-hack/proc-macro-hack-0.5.19.crate",
        "sha256": "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5",
        "dest": "cargo/vendor/proc-macro-hack-0.5.19"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5\", \"files\": {}}",
        "dest": "cargo/vendor/proc-macro-hack-0.5.19",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/proc-macro-nested/proc-macro-nested-0.1.7.crate",
        "sha256": "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086",
        "dest": "cargo/vendor/proc-macro-nested-0.1.7"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086\", \"files\": {}}",
        "dest": "cargo/vendor/proc-macro-nested-0.1.7",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/proc-macro2/proc-macro2-1.0.28.crate",
        "sha256": "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612",
        "dest": "cargo/vendor/proc-macro2-1.0.28"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612\", \"files\": {}}",
        "dest": "cargo/vendor/proc-macro2-1.0.28",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/quick-xml/quick-xml-0.22.0.crate",
        "sha256": "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b",
        "dest": "cargo/vendor/quick-xml-0.22.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b\", \"files\": {}}",
        "dest": "cargo/vendor/quick-xml-0.22.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/quote/quote-1.0.9.crate",
        "sha256": "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7",
        "dest": "cargo/vendor/quote-1.0.9"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7\", \"files\": {}}",
        "dest": "cargo/vendor/quote-1.0.9",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/rand/rand-0.8.5.crate",
        "sha256": "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404",
        "dest": "cargo/vendor/rand-0.8.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404\", \"files\": {}}",
        "dest": "cargo/vendor/rand-0.8.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/rand_chacha/rand_chacha-0.3.1.crate",
        "sha256": "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88",
        "dest": "cargo/vendor/rand_chacha-0.3.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88\", \"files\": {}}",
        "dest": "cargo/vendor/rand_chacha-0.3.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/rand_core/rand_core-0.6.3.crate",
        "sha256": "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7",
        "dest": "cargo/vendor/rand_core-0.6.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7\", \"files\": {}}",
        "dest": "cargo/vendor/rand_core-0.6.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/regex/regex-1.5.4.crate",
        "sha256": "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461",
        "dest": "cargo/vendor/regex-1.5.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461\", \"files\": {}}",
        "dest": "cargo/vendor/regex-1.5.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/regex-syntax/regex-syntax-0.6.25.crate",
        "sha256": "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b",
        "dest": "cargo/vendor/regex-syntax-0.6.25"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b\", \"files\": {}}",
        "dest": "cargo/vendor/regex-syntax-0.6.25",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/rustc_version/rustc_version-0.3.3.crate",
        "sha256": "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee",
        "dest": "cargo/vendor/rustc_version-0.3.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee\", \"files\": {}}",
        "dest": "cargo/vendor/rustc_version-0.3.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/semver/semver-0.11.0.crate",
        "sha256": "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6",
        "dest": "cargo/vendor/semver-0.11.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6\", \"files\": {}}",
        "dest": "cargo/vendor/semver-0.11.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/semver-parser/semver-parser-0.10.2.crate",
        "sha256": "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7",
        "dest": "cargo/vendor/semver-parser-0.10.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7\", \"files\": {}}",
        "dest": "cargo/vendor/semver-parser-0.10.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/serde/serde-1.0.127.crate",
        "sha256": "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8",
        "dest": "cargo/vendor/serde-1.0.127"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8\", \"files\": {}}",
        "dest": "cargo/vendor/serde-1.0.127",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/serde_derive/serde_derive-1.0.127.crate",
        "sha256": "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc",
        "dest": "cargo/vendor/serde_derive-1.0.127"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc\", \"files\": {}}",
        "dest": "cargo/vendor/serde_derive-1.0.127",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/serde_repr/serde_repr-0.1.7.crate",
        "sha256": "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5",
        "dest": "cargo/vendor/serde_repr-0.1.7"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5\", \"files\": {}}",
        "dest": "cargo/vendor/serde_repr-0.1.7",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/sha1/sha1-0.6.1.crate",
        "sha256": "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770",
        "dest": "cargo/vendor/sha1-0.6.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770\", \"files\": {}}",
        "dest": "cargo/vendor/sha1-0.6.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/sha1_smol/sha1_smol-1.0.0.crate",
        "sha256": "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012",
        "dest": "cargo/vendor/sha1_smol-1.0.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012\", \"files\": {}}",
        "dest": "cargo/vendor/sha1_smol-1.0.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/slab/slab-0.4.4.crate",
        "sha256": "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590",
        "dest": "cargo/vendor/slab-0.4.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590\", \"files\": {}}",
        "dest": "cargo/vendor/slab-0.4.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/smallvec/smallvec-1.6.1.crate",
        "sha256": "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e",
        "dest": "cargo/vendor/smallvec-1.6.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e\", \"files\": {}}",
        "dest": "cargo/vendor/smallvec-1.6.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/socket2/socket2-0.4.4.crate",
        "sha256": "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0",
        "dest": "cargo/vendor/socket2-0.4.4"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0\", \"files\": {}}",
        "dest": "cargo/vendor/socket2-0.4.4",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/static_assertions/static_assertions-1.1.0.crate",
        "sha256": "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f",
        "dest": "cargo/vendor/static_assertions-1.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f\", \"files\": {}}",
        "dest": "cargo/vendor/static_assertions-1.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/syn/syn-1.0.74.crate",
        "sha256": "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c",
        "dest": "cargo/vendor/syn-1.0.74"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c\", \"files\": {}}",
        "dest": "cargo/vendor/syn-1.0.74",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/system-deps/system-deps-6.0.2.crate",
        "sha256": "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709",
        "dest": "cargo/vendor/system-deps-6.0.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709\", \"files\": {}}",
        "dest": "cargo/vendor/system-deps-6.0.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/termcolor/termcolor-1.1.2.crate",
        "sha256": "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4",
        "dest": "cargo/vendor/termcolor-1.1.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4\", \"files\": {}}",
        "dest": "cargo/vendor/termcolor-1.1.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/thiserror/thiserror-1.0.26.crate",
        "sha256": "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2",
        "dest": "cargo/vendor/thiserror-1.0.26"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2\", \"files\": {}}",
        "dest": "cargo/vendor/thiserror-1.0.26",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.26.crate",
        "sha256": "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745",
        "dest": "cargo/vendor/thiserror-impl-1.0.26"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745\", \"files\": {}}",
        "dest": "cargo/vendor/thiserror-impl-1.0.26",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/toml/toml-0.5.8.crate",
        "sha256": "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa",
        "dest": "cargo/vendor/toml-0.5.8"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa\", \"files\": {}}",
        "dest": "cargo/vendor/toml-0.5.8",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/ucd-trie/ucd-trie-0.1.3.crate",
        "sha256": "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c",
        "dest": "cargo/vendor/ucd-trie-0.1.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c\", \"files\": {}}",
        "dest": "cargo/vendor/ucd-trie-0.1.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/unicode-xid/unicode-xid-0.2.2.crate",
        "sha256": "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3",
        "dest": "cargo/vendor/unicode-xid-0.2.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3\", \"files\": {}}",
        "dest": "cargo/vendor/unicode-xid-0.2.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/uuid/uuid-1.1.2.crate",
        "sha256": "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f",
        "dest": "cargo/vendor/uuid-1.1.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f\", \"files\": {}}",
        "dest": "cargo/vendor/uuid-1.1.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/version-compare/version-compare-0.1.0.crate",
        "sha256": "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73",
        "dest": "cargo/vendor/version-compare-0.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73\", \"files\": {}}",
        "dest": "cargo/vendor/version-compare-0.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/version_check/version_check-0.9.3.crate",
        "sha256": "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe",
        "dest": "cargo/vendor/version_check-0.9.3"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe\", \"files\": {}}",
        "dest": "cargo/vendor/version_check-0.9.3",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/waker-fn/waker-fn-1.1.0.crate",
        "sha256": "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca",
        "dest": "cargo/vendor/waker-fn-1.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca\", \"files\": {}}",
        "dest": "cargo/vendor/waker-fn-1.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/wasi/wasi-0.10.2+wasi-snapshot-preview1.crate",
        "sha256": "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6",
        "dest": "cargo/vendor/wasi-0.10.2+wasi-snapshot-preview1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6\", \"files\": {}}",
        "dest": "cargo/vendor/wasi-0.10.2+wasi-snapshot-preview1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/wepoll-ffi/wepoll-ffi-0.1.2.crate",
        "sha256": "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb",
        "dest": "cargo/vendor/wepoll-ffi-0.1.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb\", \"files\": {}}",
        "dest": "cargo/vendor/wepoll-ffi-0.1.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/winapi/winapi-0.3.9.crate",
        "sha256": "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419",
        "dest": "cargo/vendor/winapi-0.3.9"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419\", \"files\": {}}",
        "dest": "cargo/vendor/winapi-0.3.9",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/winapi-i686-pc-windows-gnu/winapi-i686-pc-windows-gnu-0.4.0.crate",
        "sha256": "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6",
        "dest": "cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6\", \"files\": {}}",
        "dest": "cargo/vendor/winapi-i686-pc-windows-gnu-0.4.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/winapi-util/winapi-util-0.1.5.crate",
        "sha256": "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178",
        "dest": "cargo/vendor/winapi-util-0.1.5"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178\", \"files\": {}}",
        "dest": "cargo/vendor/winapi-util-0.1.5",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/winapi-x86_64-pc-windows-gnu/winapi-x86_64-pc-windows-gnu-0.4.0.crate",
        "sha256": "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f",
        "dest": "cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f\", \"files\": {}}",
        "dest": "cargo/vendor/winapi-x86_64-pc-windows-gnu-0.4.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/zbus/zbus-2.1.1.crate",
        "sha256": "7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a",
        "dest": "cargo/vendor/zbus-2.1.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a\", \"files\": {}}",
        "dest": "cargo/vendor/zbus-2.1.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/zbus_macros/zbus_macros-2.1.1.crate",
        "sha256": "36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040",
        "dest": "cargo/vendor/zbus_macros-2.1.1"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040\", \"files\": {}}",
        "dest": "cargo/vendor/zbus_macros-2.1.1",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/zbus_names/zbus_names-2.1.0.crate",
        "sha256": "45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1",
        "dest": "cargo/vendor/zbus_names-2.1.0"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1\", \"files\": {}}",
        "dest": "cargo/vendor/zbus_names-2.1.0",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/zvariant/zvariant-3.1.2.crate",
        "sha256": "49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033",
        "dest": "cargo/vendor/zvariant-3.1.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033\", \"files\": {}}",
        "dest": "cargo/vendor/zvariant-3.1.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "archive",
        "archive-type": "tar-gzip",
        "url": "https://static.crates.io/crates/zvariant_derive/zvariant_derive-3.1.2.crate",
        "sha256": "8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a",
        "dest": "cargo/vendor/zvariant_derive-3.1.2"
    },
    {
        "type": "inline",
        "contents": "{\"package\": \"8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a\", \"files\": {}}",
        "dest": "cargo/vendor/zvariant_derive-3.1.2",
        "dest-filename": ".cargo-checksum.json"
    },
    {
        "type": "inline",
        "contents": "[source.vendored-sources]\ndirectory = \"cargo/vendor\"\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n",
        "dest": "cargo",
        "dest-filename": "config"
    }
]

================================================
FILE: src/resources/com.github.weclaw1.ImageRoll.desktop
================================================
[Desktop Entry]
Type=Application
Name=Image Roll
Comment=Image viewer with basic image manipulation tools
Exec=image-roll %U
Icon=com.github.weclaw1.ImageRoll
Terminal=false
StartupWMClass=image-roll
TryExec=image-roll
Categories=Graphics;
X-Purism-FormFactor=Workstation;Mobile;
MimeType=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;

================================================
FILE: src/resources/com.github.weclaw1.ImageRoll.gschema.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
  <schema id="com.github.weclaw1.ImageRoll" path="/com/github/weclaw1/ImageRoll/">
    <key name="window-width" type="u">
      <default>1024</default>
      <summary>Last window width</summary>
    </key>
    <key name="window-height" type="u">
      <default>768</default>
      <summary>Last window height</summary>
    </key>
  </schema>
</schemalist>


================================================
FILE: src/resources/com.github.weclaw1.ImageRoll.metainfo.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
  <id>com.github.weclaw1.ImageRoll</id>
  <name>Image Roll</name>
  <summary>Image viewer with basic image manipulation tools</summary>
  <metadata_license>CC0-1.0</metadata_license>
  <project_license>MIT</project_license>
  <description>
    <p>
      Image Roll is a simple and fast GTK image viewer with basic image manipulation tools. Written in rust.
    </p>
  </description>
  <launchable type="desktop-id">com.github.weclaw1.ImageRoll.desktop</launchable>
  <screenshots>
    <screenshot type="default">
      <image>https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/screenshot.png</image>
    </screenshot>
  </screenshots>
  <releases>
    <release version="2.1.0" date="2022-07-08"/>
  </releases>
  <content_rating type="oars-1.0"/>
  <developer_name>Robert Węcławski</developer_name>
  <url type="homepage">https://github.com/weclaw1/image-roll</url>
  <url type="bugtracker">https://github.com/weclaw1/image-roll/issues</url>
</component>

================================================
FILE: src/resources/com.github.weclaw1.ImageRoll.yaml
================================================
app-id: com.github.weclaw1.ImageRoll
runtime: org.gnome.Platform
runtime-version: '42'
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: image-roll
finish-args:
- --share=ipc
- --socket=fallback-x11
- --socket=wayland
- --filesystem=home
- --filesystem=/mnt
- --filesystem=/media
- --filesystem=/run/media
- --device=dri
build-options:
  append-path: /usr/lib/sdk/rust-stable/bin
  env:
    CARGO_HOME: /run/build/image-roll/cargo
modules:
- name: image-roll
  buildsystem: simple
  build-commands:
  - cargo --offline fetch --manifest-path Cargo.toml
  - cargo --offline build --release
  - install -Dm755 ./target/release/image-roll -t /app/bin/
  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.svg -t /app/share/icons/hicolor/scalable/apps/
  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.desktop -t /app/share/applications/
  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.metainfo.xml -t /app/share/metainfo/
  - install -Dm644 ./src/resources/com.github.weclaw1.ImageRoll.gschema.xml -t /app/share/glib-2.0/schemas/
  - glib-compile-schemas /app/share/glib-2.0/schemas
  sources:
  - cargo-sources.json
  - type: git
    url: https://github.com/weclaw1/image-roll.git
    tag: 2.1.0


================================================
FILE: src/resources/image-roll.cmb
================================================
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
<cambalache-project version="0.9.1" target_tk="gtk-4.0">
  <ui>
	(1,None,"image-roll.ui","image-roll.ui",None,None,None,None,None,None)
  </ui>
  <object>
	(1,1,"GtkApplicationWindow","main_window",None,None,None,None,None),
	(1,2,"GtkHeaderBar","headerbar",1,None,"titlebar",None,-1),
	(1,3,"GtkBox",None,2,None,"end",None,None),
	(1,4,"GtkBox",None,2,None,"start",None,None),
	(1,5,"GtkButton","preview_smaller_button",4,None,None,None,None),
	(1,6,"GtkLabel","preview_size_label",4,None,None,None,1),
	(1,7,"GtkButton","preview_larger_button",4,None,None,None,2),
	(1,8,"GtkButton","preview_fit_screen_button",4,None,None,None,3),
	(1,9,"GtkButton","delete_button",3,None,None,None,None),
	(1,10,"GtkMenuButton","menu_button",3,None,None,None,1),
	(1,11,"GtkPopoverMenu","popover_menu",None,None,None,None,None),
	(1,12,"GtkBox",None,11,None,None,None,None),
	(1,13,"GtkButton","open_menu_button",12,None,None,None,None),
	(1,14,"GtkButton","save_menu_button",12,None,None,None,1),
	(1,15,"GtkButton","set_as_wallpaper_menu_button",12,None,None,None,4),
	(1,16,"GtkButton","print_menu_button",12,None,None,None,5),
	(1,17,"GtkButton","copy_menu_button",12,None,None,None,3),
	(1,18,"GtkButton","save_as_menu_button",12,None,None,None,2),
	(1,23,"GtkBox",None,1,None,None,None,None),
	(1,24,"GtkInfoBar","error_info_bar",23,None,None,None,None),
	(1,25,"GtkLabel","error_info_bar_text",24,None,None,None,None),
	(1,26,"GtkScrolledWindow","image_scrolled_window",23,None,None,None,1),
	(1,27,"GtkViewport","image_viewport",26,None,None,None,None),
	(1,29,"GtkBox","action_bar",23,None,None,None,2),
	(1,30,"GtkButton","previous_button",29,None,None,None,None),
	(1,31,"GtkFlowBox",None,29,None,None,None,1),
	(1,32,"GtkButton","next_button",29,None,None,None,2),
	(1,33,"GtkButton","undo_button",31,None,None,None,None),
	(1,34,"GtkButton","rotate_counterclockwise_button",31,None,None,None,1),
	(1,36,"GtkMenuButton","resize_button",31,None,None,None,3),
	(1,37,"GtkToggleButton","crop_button",31,None,None,None,2),
	(1,38,"GtkPopover","resize_popover",None,None,None,None,None),
	(1,39,"GtkBox",None,38,None,None,None,None),
	(1,40,"GtkToggleButton","link_aspect_ratio_button",39,None,None,None,None),
	(1,41,"GtkSpinButton","width_spin_button",39,None,None,None,1),
	(1,42,"GtkAdjustment","width_adjustment",None,None,None,None,None),
	(1,43,"GtkLabel","x_label",39,None,None,None,2),
	(1,44,"GtkSpinButton","height_spin_button",39,None,None,None,3),
	(1,45,"GtkAdjustment","height_adjustment",None,None,None,None,None),
	(1,46,"GtkButton","apply_resize_button",39,None,None,None,4),
	(1,47,"GtkButton","rotate_clockwise_button",31,None,None,None,4),
	(1,48,"GtkButton","redo_button",31,None,None,None,5),
	(1,49,"GtkDrawingArea","image_widget",27,None,None,None,None)
  </object>
  <object_property>
	(1,1,"GtkWindow","child",None,None,None,None,None,23),
	(1,1,"GtkWindow","default-height","768",None,None,None,None,None),
	(1,1,"GtkWindow","default-width","1024",None,None,None,None,None),
	(1,1,"GtkWindow","icon-name","com.github.weclaw1.ImageRoll",None,None,None,None,None),
	(1,1,"GtkWindow","title","Image Roll",None,None,None,None,None),
	(1,3,"GtkBox","spacing","5",None,None,None,None,None),
	(1,5,"GtkButton","icon-name","zoom-out-symbolic",None,None,None,None,None),
	(1,6,"GtkLabel","label","Fit screen",None,None,None,None,None),
	(1,7,"GtkButton","icon-name","zoom-in-symbolic",None,None,None,None,None),
	(1,8,"GtkButton","icon-name","zoom-fit-best-symbolic",None,None,None,None,None),
	(1,9,"GtkButton","icon-name","user-trash-symbolic",None,None,None,None,None),
	(1,10,"GtkMenuButton","direction","none",None,None,None,None,None),
	(1,10,"GtkMenuButton","popover","11",None,None,None,None,None),
	(1,11,"GtkPopover","child",None,None,None,None,None,12),
	(1,12,"GtkOrientable","orientation","vertical",None,None,None,None,None),
	(1,13,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,13,"GtkButton","label","Open...",None,None,None,None,None),
	(1,14,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,14,"GtkButton","label","Save",None,None,None,None,None),
	(1,15,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,15,"GtkButton","label","Set as wallpaper",None,None,None,None,None),
	(1,16,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,16,"GtkButton","label","Print",None,None,None,None,None),
	(1,17,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,17,"GtkButton","label","Copy",None,None,None,None,None),
	(1,18,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,18,"GtkButton","label","Save as...",None,None,None,None,None),
	(1,23,"GtkOrientable","orientation","vertical",None,None,None,None,None),
	(1,24,"GtkInfoBar","message-type","error",None,None,None,None,None),
	(1,24,"GtkInfoBar","revealed","False",None,None,None,None,None),
	(1,24,"GtkInfoBar","show-close-button","True",None,None,None,None,None),
	(1,25,"GtkLabel","label","ERROR",None,None,None,None,None),
	(1,26,"GtkScrolledWindow","child",None,None,None,None,None,27),
	(1,26,"GtkWidget","vexpand","True",None,None,None,None,None),
	(1,27,"GtkViewport","child",None,None,None,None,None,49),
	(1,30,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,30,"GtkButton","icon-name","go-previous-symbolic",None,None,None,None,None),
	(1,30,"GtkWidget","halign","start",None,None,None,None,None),
	(1,30,"GtkWidget","hexpand","True",None,None,None,None,None),
	(1,31,"GtkFlowBox","column-spacing","8",None,None,None,None,None),
	(1,31,"GtkFlowBox","max-children-per-line","6",None,None,None,None,None),
	(1,31,"GtkWidget","halign","center",None,None,None,None,None),
	(1,31,"GtkWidget","width-request","300",None,None,None,None,None),
	(1,32,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,32,"GtkButton","icon-name","go-next-symbolic",None,None,None,None,None),
	(1,32,"GtkWidget","halign","end",None,None,None,None,None),
	(1,32,"GtkWidget","hexpand","True",None,None,None,None,None),
	(1,33,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,33,"GtkButton","icon-name","edit-undo-symbolic",None,None,None,None,None),
	(1,34,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,34,"GtkButton","icon-name","object-rotate-left-symbolic",None,None,None,None,None),
	(1,36,"GtkMenuButton","direction","up",None,None,None,None,None),
	(1,36,"GtkMenuButton","has-frame","False",None,None,None,None,None),
	(1,36,"GtkMenuButton","icon-name","view-fullscreen-symbolic",None,None,None,None,None),
	(1,36,"GtkMenuButton","popover","38",None,None,None,None,None),
	(1,37,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,37,"GtkButton","icon-name","crop-symbolic",None,None,None,None,None),
	(1,38,"GtkPopover","child",None,None,None,None,None,39),
	(1,38,"GtkPopover","position","top",None,None,None,None,None),
	(1,40,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,40,"GtkButton","icon-name","insert-link-symbolic",None,None,None,None,None),
	(1,41,"GtkOrientable","orientation","vertical",None,None,None,None,None),
	(1,41,"GtkSpinButton","adjustment","42",None,None,None,None,None),
	(1,41,"GtkSpinButton","climb-rate","0.5",None,None,None,None,None),
	(1,42,"GtkAdjustment","page-increment","10.0",None,None,None,None,None),
	(1,42,"GtkAdjustment","step-increment","1.0",None,None,None,None,None),
	(1,42,"GtkAdjustment","upper","2147483647.0",None,None,None,None,None),
	(1,43,"GtkLabel","label","x",None,None,None,None,None),
	(1,43,"GtkWidget","margin-end","5",None,None,None,None,None),
	(1,43,"GtkWidget","margin-start","5",None,None,None,None,None),
	(1,44,"GtkOrientable","orientation","vertical",None,None,None,None,None),
	(1,44,"GtkSpinButton","adjustment","45",None,None,None,None,None),
	(1,44,"GtkSpinButton","climb-rate","0.5",None,None,None,None,None),
	(1,45,"GtkAdjustment","page-increment","10.0",None,None,None,None,None),
	(1,45,"GtkAdjustment","step-increment","1.0",None,None,None,None,None),
	(1,45,"GtkAdjustment","upper","2147483647.0",None,None,None,None,None),
	(1,46,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,46,"GtkButton","icon-name","emblem-ok-symbolic",None,None,None,None,None),
	(1,47,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,47,"GtkButton","icon-name","object-rotate-right-symbolic",None,None,None,None,None),
	(1,48,"GtkButton","has-frame","False",None,None,None,None,None),
	(1,48,"GtkButton","icon-name","edit-redo-symbolic",None,None,None,None,None),
	(1,49,"GtkWidget","halign","center",None,None,None,None,None),
	(1,49,"GtkWidget","valign","center",None,None,None,None,None)
  </object_property>
</cambalache-project>


================================================
FILE: src/resources/image-roll.ui
================================================
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.9.1 -->
<interface>
  <!-- interface-name image-roll.ui -->
  <requires lib="gtk" version="4.6"/>
  <object class="GtkApplicationWindow" id="main_window">
    <property name="child">
      <object class="GtkBox">
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkInfoBar" id="error_info_bar">
            <property name="message-type">error</property>
            <property name="revealed">False</property>
            <property name="show-close-button">True</property>
            <child>
              <object class="GtkLabel" id="error_info_bar_text">
                <property name="label">ERROR</property>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkScrolledWindow" id="image_scrolled_window">
            <property name="child">
              <object class="GtkViewport" id="image_viewport">
                <property name="child">
                  <object class="GtkDrawingArea" id="image_widget">
                    <property name="halign">center</property>
                    <property name="valign">center</property>
                  </object>
                </property>
              </object>
            </property>
            <property name="vexpand">True</property>
          </object>
        </child>
        <child>
          <object class="GtkBox" id="action_bar">
            <child>
              <object class="GtkButton" id="previous_button">
                <property name="halign">start</property>
                <property name="has-frame">False</property>
                <property name="hexpand">True</property>
                <property name="icon-name">go-previous-symbolic</property>
              </object>
            </child>
            <child>
              <object class="GtkFlowBox">
                <property name="column-spacing">8</property>
                <property name="halign">center</property>
                <property name="max-children-per-line">6</property>
                <property name="width-request">300</property>
                <child>
                  <object class="GtkButton" id="undo_button">
                    <property name="has-frame">False</property>
                    <property name="icon-name">edit-undo-symbolic</property>
                  </object>
                </child>
                <child>
                  <object class="GtkButton" id="rotate_counterclockwise_button">
                    <property name="has-frame">False</property>
                    <property name="icon-name">object-rotate-left-symbolic</property>
                  </object>
                </child>
                <child>
                  <object class="GtkToggleButton" id="crop_button">
                    <property name="has-frame">False</property>
                    <property name="icon-name">crop-symbolic</property>
                  </object>
                </child>
                <child>
                  <object class="GtkMenuButton" id="resize_button">
                    <property name="direction">up</property>
                    <property name="has-frame">False</property>
                    <property name="icon-name">view-fullscreen-symbolic</property>
                    <property name="popover">resize_popover</property>
                  </object>
                </child>
                <child>
                  <object class="GtkButton" id="rotate_clockwise_button">
                    <property name="has-frame">False</property>
                    <property name="icon-name">object-rotate-right-symbolic</property>
                  </object>
                </child>
                <child>
                  <object class="GtkButton" id="redo_button">
                    <property name="has-frame">False</property>
                    <property name="icon-name">edit-redo-symbolic</property>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkButton" id="next_button">
                <property name="halign">end</property>
                <property name="has-frame">False</property>
                <property name="hexpand">True</property>
                <property name="icon-name">go-next-symbolic</property>
              </object>
            </child>
          </object>
        </child>
      </object>
    </property>
    <property name="default-height">768</property>
    <property name="default-width">1024</property>
    <property name="icon-name">com.github.weclaw1.ImageRoll</property>
    <property name="title">Image Roll</property>
    <child type="titlebar">
      <object class="GtkHeaderBar" id="headerbar">
        <child type="end">
          <object class="GtkBox">
            <property name="spacing">5</property>
            <child>
              <object class="GtkButton" id="delete_button">
                <property name="icon-name">user-trash-symbolic</property>
              </object>
            </child>
            <child>
              <object class="GtkMenuButton" id="menu_button">
                <property name="direction">none</property>
                <property name="popover">popover_menu</property>
              </object>
            </child>
          </object>
        </child>
        <child type="start">
          <object class="GtkBox">
            <child>
              <object class="GtkButton" id="preview_smaller_button">
                <property name="icon-name">zoom-out-symbolic</property>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="preview_size_label">
                <property name="label">Fit screen</property>
              </object>
            </child>
            <child>
              <object class="GtkButton" id="preview_larger_button">
                <property name="icon-name">zoom-in-symbolic</property>
              </object>
            </child>
            <child>
              <object class="GtkButton" id="preview_fit_screen_button">
                <property name="icon-name">zoom-fit-best-symbolic</property>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </object>
  <object class="GtkPopoverMenu" id="popover_menu">
    <property name="child">
      <object class="GtkBox">
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkButton" id="open_menu_button">
            <property name="has-frame">False</property>
            <property name="label">Open...</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="save_menu_button">
            <property name="has-frame">False</property>
            <property name="label">Save</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="save_as_menu_button">
            <property name="has-frame">False</property>
            <property name="label">Save as...</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="copy_menu_button">
            <property name="has-frame">False</property>
            <property name="label">Copy</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="set_as_wallpaper_menu_button">
            <property name="has-frame">False</property>
            <property name="label">Set as wallpaper</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="print_menu_button">
            <property name="has-frame">False</property>
            <property name="label">Print</property>
          </object>
        </child>
      </object>
    </property>
  </object>
  <object class="GtkPopover" id="resize_popover">
    <property name="child">
      <object class="GtkBox">
        <child>
          <object class="GtkToggleButton" id="link_aspect_ratio_button">
            <property name="has-frame">False</property>
            <property name="icon-name">insert-link-symbolic</property>
          </object>
        </child>
        <child>
          <object class="GtkSpinButton" id="width_spin_button">
            <property name="adjustment">width_adjustment</property>
            <property name="climb-rate">0.5</property>
            <property name="orientation">vertical</property>
          </object>
        </child>
        <child>
          <object class="GtkLabel" id="x_label">
            <property name="label">x</property>
            <property name="margin-end">5</property>
            <property name="margin-start">5</property>
          </object>
        </child>
        <child>
          <object class="GtkSpinButton" id="height_spin_button">
            <property name="adjustment">height_adjustment</property>
            <property name="climb-rate">0.5</property>
            <property name="orientation">vertical</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="apply_resize_button">
            <property name="has-frame">False</property>
            <property name="icon-name">emblem-ok-symbolic</property>
          </object>
        </child>
      </object>
    </property>
    <property name="position">top</property>
  </object>
  <object class="GtkAdjustment" id="width_adjustment">
    <property name="page-increment">10.0</property>
    <property name="step-increment">1.0</property>
    <property name="upper">2147483647.0</property>
  </object>
  <object class="GtkAdjustment" id="height_adjustment">
    <property name="page-increment">10.0</property>
    <property name="step-increment">1.0</property>
    <property name="upper">2147483647.0</property>
  </object>
</interface>


================================================
FILE: src/resources/resources.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/github/weclaw1/image-roll">
    <file>image-roll.ui</file>
    <file>com.github.weclaw1.ImageRoll.svg</file>
  </gresource>
  <gresource prefix="/com/github/weclaw1/image-roll/icons/scalable/actions/">
    <file preprocess="xml-stripblanks" alias="crop-symbolic.svg">icons/crop-symbolic.svg</file>
  </gresource>
</gresources>


================================================
FILE: src/settings.rs
================================================
use gtk::gio;
use gtk::gio::prelude::SettingsExt;
use gtk::gio::SettingsSchemaSource;

use crate::image::PreviewSize;

#[derive(Clone)]
pub struct Settings {
    gio_settings: Option<gio::Settings>,
    scale: PreviewSize,
    scale_before_zoom_gesture: Option<PreviewSize>,
    fullscreen: bool,
}

impl Settings {
    pub fn new(application_id: &str) -> Settings {
        let gio_settings = match SettingsSchemaSource::default() {
            Some(schema_source) => {
                if schema_source.lookup(application_id, true).is_some() {
                    Some(gio::Settings::new(application_id))
                } else {
                    None
                }
            }
            None => None,
        };

        Settings {
            gio_settings,
            scale: PreviewSize::BestFit(0, 0),
            scale_before_zoom_gesture: None,
            fullscreen: false,
        }
    }

    pub fn set_window_size(&self, window_size: (u32, u32)) {
        if let Some(gio_settings) = self.gio_settings.as_ref() {
            let (window_width, window_height) = window_size;
            gio_settings
                .set_uint("window-width", window_width)
                .expect("Could not set setting window-width.");
            gio_settings
                .set_uint("window-height", window_height)
                .expect("Could not set setting window-height.");
        }
    }

    pub fn window_size(&self) -> (u32, u32) {
        match self.gio_settings.as_ref() {
            Some(gio_settings) => (
                gio_settings.uint("window-width"),
                gio_settings.uint("window-height"),
            ),
            None => (1024, 768),
        }
    }

    pub fn set_scale(&mut self, preview_size: PreviewSize) {
        self.scale = preview_size;
    }

    pub fn scale(&self) -> PreviewSize {
        self.scale
    }

    pub fn set_fullscreen(&mut self, fullscreen: bool) {
        self.fullscreen = fullscreen;
    }

    pub fn fullscreen(&self) -> bool {
        self.fullscreen
    }

    pub fn scale_before_zoom_gesture(&self) -> Option<PreviewSize> {
        self.scale_before_zoom_gesture
    }

    pub fn set_scale_before_zoom_gesture(
        &mut self,
        scale_before_zoom_gesture: Option<PreviewSize>,
    ) {
        self.scale_before_zoom_gesture = scale_before_zoom_gesture;
    }
}


================================================
FILE: src/test_utils.rs
================================================
use std::path::{Path, PathBuf};

pub struct TestResources {
    file_folder: PathBuf,
}

impl TestResources {
    pub fn new<P: AsRef<Path>>(file_folder: P) -> Self {
        std::fs::create_dir_all(&file_folder).unwrap();
        Self {
            file_folder: file_folder.as_ref().to_path_buf(),
        }
    }

    pub fn add_file<T: AsRef<str>, C: AsRef<[u8]>>(&mut self, file_name: T, contents: C) {
        std::fs::write(self.file_folder.join(file_name.as_ref()), contents).unwrap();
    }

    pub fn remove_file<T: AsRef<str>>(&mut self, file_name: T) {
        std::fs::remove_file(self.file_folder.join(file_name.as_ref())).unwrap();
    }

    pub fn file_folder(&self) -> &Path {
        self.file_folder.as_path()
    }
}

impl Drop for TestResources {
    fn drop(&mut self) {
        std::fs::remove_dir_all(self.file_folder.as_path()).unwrap();
    }
}


================================================
FILE: src/ui/action.rs
================================================
use std::{
    cell::{Cell, RefCell},
    path::PathBuf,
    rc::Rc,
};

#[cfg(feature = "wallpaper")]
use ashpd::{
    desktop::wallpaper::{self, SetOn},
    WindowIdentifier,
};
use gtk::{
    gdk, gio,
    glib::{self, timeout_future_seconds, Sender},
    prelude::{
        DisplayExt, FileMonitorExt, GdkCairoContextExt, GtkApplicationExt, GtkWindowExt,
        PrintOperationExt, ToggleButtonExt, WidgetExt,
    },
    traits::DrawingAreaExt,
    MessageType,
};

use crate::{
    file_list::FileList,
    image::{self, CoordinatesPair, PreviewSize},
    image_list::ImageList,
    image_operation::{ApplyImageOperation, ImageOperation},
    settings::Settings,
};

use super::{
    event::{post_event, Event},
    widgets::Widgets,
};

pub fn refresh_file_list(sender: &Sender<Event>, file_list: &mut FileList) {
    post_event(sender, Event::HideInfoPanel);
    if let Err(error) = file_list.refresh() {
        post_event(
            sender,
            Event::DisplayMessage(error.to_string(), MessageType::Error),
        );
        return;
    };

    post_event(sender, Event::LoadImage(file_list.current_file_path()));
}

pub fn open_file(
    sender: &Sender<Event>,
    image_list: Rc<RefCell<ImageList>>,
    file_list: &mut FileList,
    file: gio::File,
) {
    post_event(sender, Event::HideInfoPanel);
    image_list.replace(ImageList::new());

    let new_file_list = match FileList::new(Some(file)) {
        Ok(file_list) => file_list,
        Err(error) => {
            post_event(
                sender,
                Event::DisplayMessage(error.to_string(), MessageType::Error),
            );
            return;
        }
    };

    *file_list = new_file_list;

    post_event(sender, Event::LoadImage(file_list.current_file_path()));

    let sender = sender.clone();
    file_list
        .current_folder_monitor_mut()
        .unwrap()
        .connect_changed(move |_, _, _, _| {
            post_event(&sender, Event::RefreshFileList);
        });
}

pub fn load_image(
    sender: &Sender<Event>,
    settings: &mut Settings,
    widgets: &Widgets,
    image_list: Rc<RefCell<ImageList>>,
    file_path: Option<PathBuf>,
) {
    hide_info_panel(widgets);
    let mut image_list = image_list.borrow_mut();
    if let Some(file_path) = file_path {
        let image = if let Some(image) = image_list.remove(&file_path) {
            image.reload(&file_path)
        } else {
            image::Image::load(&file_path)
        };
        let image = match image {
            Ok(image) => image,
            Err(error) => {
                image_list.set_current_image_path(None);
                post_event(sender, Event::RefreshPreview(settings.scale()));
                post_event(
                    sender,
                    Event::DisplayMessage(error.to_string(), MessageType::Error),
                );
                return;
            }
        };
        image_list.insert(file_path.clone(), image);
        widgets.window().set_title(
            file_path
                .file_name()
                .and_then(|file_name| file_name.to_str()),
        );
        image_list.set_current_image_path(Some(file_path));
        if let PreviewSize::BestFit(0, 0) = settings.scale() {
            let new_scale = PreviewSize::BestFit(
                widgets.image_viewport().allocation().width() as u32,
                widgets.image_viewport().allocation().height() as u32,
            );
            settings.set_scale(new_scale);
        }
        post_event(sender, Event::RefreshPreview(settings.scale()));
    } else {
        widgets.window().set_title(Some("Image Roll"));
        image_list.set_current_image_path(None);
        post_event(sender, Event::RefreshPreview(settings.scale()));
    }
}

pub fn next_image(
    sender: &Sender<Event>,
    image_list: Rc<RefCell<ImageList>>,
    file_list: &mut FileList,
) {
    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {
        current_image.remove_image_buffers();
    }
    file_list.next();
    post_event(sender, Event::LoadImage(file_list.current_file_path()));
}

pub fn previous_image(
    sender: &Sender<Event>,
    image_list: Rc<RefCell<ImageList>>,
    file_list: &mut FileList,
) {
    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {
        current_image.remove_image_buffers();
    }
    file_list.previous();
    post_event(sender, Event::LoadImage(file_list.current_file_path()));
}

pub fn image_viewport_resize(
    sender: &Sender<Event>,
    settings: &mut Settings,
    viewport_size: (u32, u32),
) {
    if let PreviewSize::BestFit(_, _) = settings.scale() {
        let new_scale = PreviewSize::BestFit(viewport_size.0, viewport_size.1);
        settings.set_scale(new_scale);
        post_event(sender, Event::RefreshPreview(new_scale));
    }
}

pub fn refresh_preview(
    widgets: &Widgets,
    image_list: Rc<RefCell<ImageList>>,
    preview_size: PreviewSize,
) {
    widgets
        .preview_size_label()
        .set_text(String::from(preview_size).as_str());
    if let Some(image) = image_list.borrow_mut().current_image_mut() {
        image.create_preview_image_buffer(preview_size);
        if let Some((preview_image_width, preview_image_height)) = image.preview_image_buffer_size()
        {
            widgets
                .image_widget()
                .set_content_width(preview_image_width as i32);
            widgets
                .image_widget()
                .set_content_height(preview_image_height as i32);
        }
    } else {
        widgets.image_widget().set_content_width(0);
        widgets.image_widget().set_content_height(0);
    }
    widgets.image_widget().queue_draw();
}

pub fn change_preview_size(
    sender: &Sender<Event>,
    widgets: &Widgets,
    settings: &mut Settings,
    mut preview_size: PreviewSize,
) {
    if let PreviewSize::BestFit(_, _) = preview_size {
        let viewport_allocation = widgets.image_viewport().allocation();
        preview_size = PreviewSize::BestFit(
            viewport_allocation.width() as u32,
            viewport_allocation.height() as u32,
        );
    }
    settings.set_scale(preview_size);
    post_event(sender, Event::RefreshPreview(preview_size));
}

pub fn preview_smaller(sender: &Sender<Event>, settings: &Settings, value: Option<u32>) {
    let new_scale = match value {
        None => settings.scale().smaller(),
        Some(value) => settings.scale().smaller_by(value),
    };
    if let Some(new_scale) = new_scale {
        post_event(sender, Event::ChangePreviewSize(new_scale));
    }
}

pub fn preview_larger(sender: &Sender<Event>, settings: &Settings, value: Option<u32>) {
    let new_scale = match value {
        None => settings.scale().larger(),
        Some(value) => settings.scale().larger_by(value),
    };
    if let Some(new_scale) = new_scale {
        post_event(sender, Event::ChangePreviewSize(new_scale));
    }
}

pub fn preview_fit_screen(sender: &Sender<Event>) {
    let new_scale = PreviewSize::BestFit(0, 0);
    post_event(sender, Event::ChangePreviewSize(new_scale));
}

pub fn image_edit(
    sender: &Sender<Event>,
    settings: &Settings,
    image_list: Rc<RefCell<ImageList>>,
    file_list: &FileList,
    image_operation: ImageOperation,
) {
    let mut image_list = image_list.borrow_mut();
    if let Some(mut current_image) = image_list.remove_current_image() {
        current_image = current_image.apply_operation(&image_operation);
        image_list.insert(file_list.current_file_path().unwrap(), current_image);
        post_event(sender, Event::RefreshPreview(settings.scale()));
    }
}

pub fn start_selection(
    widgets: &Widgets,
    image_list: Rc<RefCell<ImageList>>,
    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,
    position: (u32, u32),
) {
    if image_list.borrow().current_image().is_some() {
        selection_coords.replace(Some((position, position)));
        widgets.image_widget().queue_draw();
    }
}

pub fn drag_selection(
    widgets: &Widgets,
    image_list: Rc<RefCell<ImageList>>,
    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,
    position: (u32, u32),
) {
    if let Some(((start_position_x, start_position_y), (_, _))) = selection_coords.get() {
        if let Some(current_image) = image_list.borrow().current_image() {
            let (position_x, position_y) = position;
            let (image_width, image_height) = current_image.preview_image_buffer_size().unwrap();
            if position_x >= image_width || position_y >= image_height {
                return;
            }
            selection_coords.replace(Some(((start_position_x, start_position_y), position)));
            widgets.image_widget().queue_draw();
        }
    }
}

pub fn end_selection(
    sender: &Sender<Event>,
    widgets: &Widgets,
    image_list: Rc<RefCell<ImageList>>,
    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,
) {
    if let Some(selection_coords) = selection_coords.take() {
        if let Some(current_image) = image_list.borrow().current_image() {
            let crop_operation = ImageOperation::Crop(
                current_image
                    .preview_coords_to_image_coords(selection_coords)
                    .unwrap(),
            );
            post_event(sender, Event::ImageEdit(crop_operation));

            widgets.image_widget().queue_draw();
            widgets.crop_button().set_active(false);
        }
    }
}

pub fn resize_popover_displayed(widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {
    if let Some(current_image) = image_list.borrow().current_image() {
        let (image_width, image_height) = current_image.image_size().unwrap();
        widgets.width_spin_button().set_value(image_width as f64);
        widgets.height_spin_button().set_value(image_height as f64);
    }
}

pub fn update_resize_popover_width(widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {
    if let Some(current_image) = image_list.borrow().current_image() {
        let aspect_ratio = current_image.image_aspect_ratio().unwrap();
        widgets
            .width_spin_button()
            .set_value(widgets.height_spin_button().value() * aspect_ratio);
    }
}

pub fn update_resize_popover_height(widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {
    if let Some(current_image) = image_list.borrow().current_image() {
        let aspect_ratio = current_image.image_aspect_ratio().unwrap();
        widgets
            .height_spin_button()
            .set_value(widgets.width_spin_button().value() / aspect_ratio);
    }
}

pub fn save_current_image(
    sender: &Sender<Event>,
    image_list: Rc<RefCell<ImageList>>,
    filename: Option<PathBuf>,
) {
    if let Err(error) = image_list.borrow_mut().save_current_image(filename) {
        post_event(
            sender,
            Event::DisplayMessage(error.to_string(), MessageType::Error),
        );
    }
}

pub fn delete_current_image(
    sender: &Sender<Event>,
    file_list: &mut FileList,
    image_list: Rc<RefCell<ImageList>>,
) {
    match file_list.delete_current_file() {
        Ok(image_path) => {
            image_list.borrow_mut().remove(image_path.as_path());
            post_event(
                sender,
                Event::DisplayMessage(
                    format!(
                        "Image {} was moved to trash",
                        image_path
                            .file_name()
                            .and_then(|file_name| file_name.to_str())
                            .unwrap_or_default()
                    ),
                    MessageType::Info,
                ),
            )
        }
        Err(error) => post_event(
            sender,
            Event::DisplayMessage(error.to_string(), MessageType::Error),
        ),
    }
}

pub fn print(sender: &Sender<Event>, widgets: &Widgets, image_list: Rc<RefCell<ImageList>>) {
    let print_operation = gtk::PrintOperation::new();

    print_operation.connect_begin_print(move |print_operation, _| {
        print_operation.set_n_pages(1);
    });

    let cloned_sender = sender.clone();
    print_operation.connect_draw_page(move |_, print_context, _| {
        if let Some(print_image_buffer) =
            image_list
                .borrow()
                .current_image()
                .and_then(|current_image| {
                    current_image.create_print_image_buffer(
                        print_context.width() as u32,
                        print_context.height() as u32,
                    )
                })
        {
            let cairo_context = print_context.cairo_context();
            cairo_context.set_source_pixbuf(
                &print_image_buffer,
                (print_context.width() - print_image_buffer.width() as f64) / 2.0,
                (print_context.height() - print_image_buffer.height() as f64) / 2.0,
            );
            if let Err(error) = cairo_context.paint() {
                post_event(
                    &cloned_sender,
                    Event::DisplayMessage(
                        format!("Couldn't print current image: {}", error),
                        MessageType::Error,
                    ),
                );
            }
        }
    });

    print_operation.set_allow_async(true);
    if let Err(error) = print_operation.run(
        gtk::PrintOperationAction::PrintDialog,
        Option::from(widgets.window()),
    ) {
        post_event(
            sender,
            Event::DisplayMessage(
                format!("Couldn't print current image: {}", error),
                MessageType::Error,
            ),
        );
    };
}

pub fn undo_operation(
    sender: &Sender<Event>,
    settings: &Settings,
    image_list: Rc<RefCell<ImageList>>,
) {
    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {
        current_image.undo_operation();
        post_event(sender, Event::RefreshPreview(settings.scale()));
    }
}

pub fn redo_operation(
    sender: &Sender<Event>,
    settings: &Settings,
    image_list: Rc<RefCell<ImageList>>,
) {
    if let Some(current_image) = image_list.borrow_mut().current_image_mut() {
        current_image.redo_operation();
        post_event(sender, Event::RefreshPreview(settings.scale()));
    }
}

pub fn display_message(widgets: &Widgets, message: &str, message_type: gtk::MessageType) {
    match message_type {
        MessageType::Error => error!("{}", message),
        MessageType::Warning => warn!("{}", message),
        MessageType::Info => info!("{}", message),
        _ => info!("{}", message),
    };
    widgets.info_bar().set_message_type(message_type);
    widgets.info_bar_text().set_text(message);
    widgets.info_bar().set_revealed(true);
    let main_context = glib::MainContext::default();
    let info_bar = widgets.info_bar().clone();
    main_context.spawn_local(async move {
        timeout_future_seconds(5).await;
        info_bar.set_revealed(false);
    });
}

pub fn hide_info_panel(widgets: &Widgets) {
    if widgets.info_bar().message_type() != gtk::MessageType::Info
        && widgets.info_bar().message_type() != gtk::MessageType::Warning
    {
        widgets.info_bar().set_revealed(false);
    }
}

pub fn toggle_fullscreen(widgets: &Widgets, settings: &mut Settings) {
    if !settings.fullscreen() {
        widgets.window().fullscreen();
        settings.set_fullscreen(true);
    } else {
        widgets.window().unfullscreen();
        settings.set_fullscreen(false);
    }
}

pub fn quit(application: &gtk::Application) {
    application
        .windows()
        .iter()
        .for_each(|window| window.close());
}

#[cfg(feature = "wallpaper")]
pub fn set_as_wallpaper(sender: &Sender<Event>, file_list: &FileList) {
    if let Some(current_file_uri) = file_list.current_file_uri() {
        let sender = sender.clone();
        let main_context = glib::MainContext::default();
        main_context.spawn_local(async move {
            if let Err(error) = wallpaper::set_from_uri(
                &WindowIdentifier::default(),
                current_file_uri.as_str(),
                true,
                SetOn::Background,
            )
            .await
            {
                post_event(
                    &sender,
                    Event::DisplayMessage(error.to_string(), MessageType::Error),
                );
            }
        });
    }
}
#[cfg(not(feature = "wallpaper"))]
pub fn set_as_wallpaper(_sender: &Sender<Event>, _file_list: &FileList) {
    error!("This program was built without the wallpaper feature");
}

pub fn copy_current_image(image_list: Rc<RefCell<ImageList>>) {
    let display = gdk::Display::default().unwrap();
    image_list.borrow().copy_current_image(display.clipboard());
}

pub fn start_zoom_gesture(settings: &mut Settings) {
    settings.set_scale_before_zoom_gesture(Some(settings.scale()));
}

pub fn change_scale_on_zoom_gesture(sender: &Sender<Event>, settings: &Settings, zoom_scale: f64) {
    if let Some(scale_before_zoom_gesture) = settings.scale_before_zoom_gesture() {
        let new_preview_size = match scale_before_zoom_gesture {
            PreviewSize::BestFit(_, _) | PreviewSize::OriginalSize => {
                PreviewSize::Resized((zoom_scale * 100.0) as u32)
            }
            PreviewSize::Resized(old_scale) => {
                PreviewSize::Resized((old_scale as f64 * zoom_scale) as u32)
            }
        };
        post_event(sender, Event::ChangePreviewSize(new_preview_size));
    }
}

pub fn update_buttons_state(
    widgets: &Widgets,
    file_list: &FileList,
    image_list: Rc<RefCell<ImageList>>,
    settings: &Settings,
) {
    let previous_next_active = file_list.len() > 1;
    widgets.next_button().set_sensitive(previous_next_active);
    widgets
        .previous_button()
        .set_sensitive(previous_next_active);

    let buttons_active = if let Some(current_image) = image_list.borrow().current_image() {
        widgets
            .undo_button()
            .set_sensitive(current_image.can_undo_operation());
        widgets
            .redo_button()
            .set_sensitive(current_image.can_redo_operation());
        widgets
            .save_menu_button()
            .set_sensitive(current_image.has_operations());
        true
    } else {
        widgets.undo_button().set_sensitive(false);
        widgets.redo_button().set_sensitive(false);
        widgets.save_menu_button().set_sensitive(false);
        false
    };

    widgets
        .rotate_counterclockwise_button()
        .set_sensitive(buttons_active);
    widgets
        .rotate_clockwise_button()
        .set_sensitive(buttons_active);
    widgets.crop_button().set_sensitive(buttons_active);
    widgets.resize_button().set_sensitive(buttons_active);
    widgets.print_menu_button().set_sensitive(buttons_active);
    widgets.save_as_menu_button().set_sensitive(buttons_active);
    widgets.delete_button().set_sensitive(buttons_active);

    #[cfg(feature = "wallpaper")]
    widgets
        .set_as_wallpaper_menu_button()
        .set_sensitive(buttons_active);
    #[cfg(not(feature = "wallpaper"))]
    widgets.set_as_wallpaper_menu_button().set_sensitive(false);

    widgets.copy_menu_button().set_sensitive(buttons_active);

    widgets
        .preview_smaller_button()
        .set_sensitive(settings.scale().can_be_smaller());
    widgets
        .preview_larger_button()
        .set_sensitive(settings.scale().can_be_larger());
}


================================================
FILE: src/ui/controllers.rs
================================================
use gtk::EventControllerScrollFlags;

#[derive(Clone)]
pub struct Controllers {
    window_key_event_controller: gtk::EventControllerKey,
    image_click_gesture: gtk::GestureClick,
    image_motion_event_controller: gtk::EventControllerMotion,
    image_zoom_gesture: gtk::GestureZoom,
    image_scrolled_window_scroll_controller: gtk::EventControllerScroll,
}

impl Controllers {
    pub fn init() -> Self {
        Self {
            window_key_event_controller: gtk::EventControllerKey::new(),
            image_click_gesture: gtk::GestureClick::new(),
            image_motion_event_controller: gtk::EventControllerMotion::new(),
            image_zoom_gesture: gtk::GestureZoom::new(),
            image_scrolled_window_scroll_controller: gtk::EventControllerScroll::new(
                EventControllerScrollFlags::BOTH_AXES,
            ),
        }
    }

    pub fn image_click_gesture(&self) -> &gtk::GestureClick {
        &self.image_click_gesture
    }

    pub fn image_motion_event_controller(&self) -> &gtk::EventControllerMotion {
        &self.image_motion_event_controller
    }

    pub fn image_zoom_gesture(&self) -> &gtk::GestureZoom {
        &self.image_zoom_gesture
    }

    pub fn window_key_event_controller(&self) -> &gtk::EventControllerKey {
        &self.window_key_event_controller
    }

    pub fn image_scrolled_window_scroll_controller(&self) -> &gtk::EventControllerScroll {
        &self.image_scrolled_window_scroll_controller
    }
}


================================================
FILE: src/ui/event.rs
================================================
use gtk::{
    gdk::{self, Key},
    gdk_pixbuf::PixbufRotation,
    gio,
    glib::{self, timeout_future, Sender},
    prelude::{
        ButtonExt, DrawingAreaExtManual, FileChooserExt, FileExt, GdkCairoContextExt,
        NativeDialogExt, PopoverExt, ToggleButtonExt, WidgetExt,
    },
    traits::{GestureExt, GestureSingleExt, GtkWindowExt},
    MessageType, Window,
};
use std::{
    cell::{Cell, RefCell},
    path::PathBuf,
    rc::Rc,
    time::Duration,
};

use crate::{
    image::{CoordinatesPair, PreviewSize},
    image_list::ImageList,
    image_operation::ImageOperation,
    settings::Settings,
};

use super::{controllers::Controllers, widgets::Widgets};

#[derive(Debug)]
pub enum Event {
    OpenFile(gio::File),
    LoadImage(Option<PathBuf>),
    ImageViewportResize((u32, u32)),
    RefreshPreview(PreviewSize),
    ChangePreviewSize(PreviewSize),
    ImageEdit(ImageOperation),
    StartSelection((u32, u32)),
    DragSelection((u32, u32)),
    SaveCurrentImage(Option<PathBuf>),
    DeleteCurrentImage,
    EndSelection,
    StartZoomGesture,
    ZoomGestureScaleChanged(f64),
    PreviewSmaller(Option<u32>),
    PreviewLarger(Option<u32>),
    PreviewFitScreen,
    NextImage,
    PreviousImage,
    RefreshFileList,
    ResizePopoverDisplayed,
    UpdateResizePopoverWidth,
    UpdateResizePopoverHeight,
    UndoOperation,
    RedoOperation,
    Print,
    DisplayMessage(String, gtk::MessageType),
    HideInfoPanel,
    ToggleFullscreen,
    CopyCurrentImage,
    Quit,
    SetAsWallpaper,
}

pub fn post_event(sender: &glib::Sender<Event>, action: Event) {
    if let Err(err) = sender.send(action) {
        error!("Send error: {}", err);
    }
}

pub fn connect_events(
    widgets: Widgets,
    sender: Sender<Event>,
    image_list: Rc<RefCell<ImageList>>,
    selection_coords: Rc<Cell<Option<CoordinatesPair>>>,
    settings: Settings,
) {
    connect_open_menu_button_clicked(widgets.clone(), sender.clone());
    connect_next_button_clicked(widgets.clone(), sender.clone());
    connect_previous_button_clicked(widgets.clone(), sender.clone());
    connect_window_default_width_notify(widgets.clone(), settings.clone(), sender.clone());
    connect_window_default_height_notify(widgets.clone(), settings, sender.clone());
    connect_window_maximized_notify(widgets.clone(), sender.clone());
    connect_window_fullscreened_notify(widgets.clone(), sender.clone());
    connect_preview_smaller_button_clicked(widgets.clone(), sender.clone());
    connect_preview_larger_button_clicked(widgets.clone(), sender.clone());
    connect_preview_fit_screen_button_clicked(widgets.clone(), sender.clone());
    connect_rotate_counterclockwise_button_clicked(widgets.clone(), sender.clone());
    connect_rotate_clockwise_button_clicked(widgets.clone(), sender.clone());
    connect_image_widget_draw(widgets.clone(), image_list.clone(), selection_coords);
    connect_resize_button_activated(widgets.clone(), sender.clone());
    connect_width_spin_button_value_changed(widgets.clone(), sender.clone());
    connect_height_spin_button_value_changed(widgets.clone(), sender.clone());
    connect_apply_resize_button_clicked(widgets.clone(), sender.clone());
    connect_save_menu_button_clicked(widgets.clone(), sender.clone());
    connect_print_menu_button_clicked(widgets.clone(), sender.clone());
    connect_undo_button_clicked(widgets.clone(), sender.clone());
    connect_redo_button_clicked(widgets.clone(), sender.clone());
    connect_save_as_menu_button_clicked(widgets.clone(), image_list, sender.clone());
    connect_delete_button_clicked(widgets.clone(), sender.clone());
    connect_info_bar_response(widgets.clone());
    connect_set_as_wallpaper_menu_button_clicked(widgets.clone()
Download .txt
gitextract_s858hyto/

├── .github/
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── build.rs
├── debian/
│   └── postinst
└── src/
    ├── app.rs
    ├── file_list.rs
    ├── image.rs
    ├── image_list.rs
    ├── image_operation.rs
    ├── main.rs
    ├── resources/
    │   ├── cargo-sources.json
    │   ├── com.github.weclaw1.ImageRoll.desktop
    │   ├── com.github.weclaw1.ImageRoll.gschema.xml
    │   ├── com.github.weclaw1.ImageRoll.metainfo.xml
    │   ├── com.github.weclaw1.ImageRoll.yaml
    │   ├── image-roll.cmb
    │   ├── image-roll.ui
    │   ├── resources.gresource
    │   └── resources.xml
    ├── settings.rs
    ├── test_utils.rs
    ├── ui/
    │   ├── action.rs
    │   ├── controllers.rs
    │   ├── event.rs
    │   └── widgets.rs
    └── ui.rs
Download .txt
SYMBOL INDEX (232 symbols across 13 files)

FILE: build.rs
  function main (line 3) | fn main() {

FILE: src/app.rs
  type App (line 27) | pub struct App {
    method create (line 39) | pub fn create(application: &gtk::Application, file: Option<&gio::File>) {
    method process_event (line 116) | pub fn process_event(&mut self, event: Event) {

FILE: src/file_list.rs
  type FileList (line 9) | pub struct FileList {
    method new (line 17) | pub fn new(current_file: Option<gio::File>) -> Result<FileList> {
    method refresh (line 70) | pub fn refresh(&mut self) -> Result<()> {
    method next (line 105) | pub fn next(&mut self) {
    method previous (line 124) | pub fn previous(&mut self) {
    method current_file (line 148) | pub fn current_file(&self) -> Option<&gio::File> {
    method current_file_uri (line 153) | pub fn current_file_uri(&self) -> Option<String> {
    method current_file_path (line 159) | pub fn current_file_path(&self) -> Option<PathBuf> {
    method len (line 163) | pub fn len(&self) -> usize {
    method enumerate_files (line 167) | fn enumerate_files(folder: &gio::File) -> Result<Vec<gio::FileInfo>> {
    method current_folder_monitor_mut (line 185) | pub fn current_folder_monitor_mut(&mut self) -> Option<&mut gio::FileM...
    method delete_current_file (line 189) | pub fn delete_current_file(&mut self) -> Result<PathBuf> {
  constant TEST_IMAGE (line 215) | const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");
  function file_list_contains_image_files (line 218) | fn file_list_contains_image_files() {
  function file_list_does_not_contain_other_files (line 232) | fn file_list_does_not_contain_other_files() {
  function file_list_contains_images_without_extension (line 247) | fn file_list_contains_images_without_extension() {
  function file_list_does_not_contain_other_files_without_extension (line 262) | fn file_list_does_not_contain_other_files_without_extension() {
  function file_list_is_in_alphabetical_order (line 279) | fn file_list_is_in_alphabetical_order() {
  function refresh_file_list_loads_new_images (line 323) | fn refresh_file_list_loads_new_images() {
  function refresh_file_list_removes_deleted_images (line 340) | fn refresh_file_list_removes_deleted_images() {
  function test_change_to_next_image (line 359) | fn test_change_to_next_image() {
  function test_change_to_previous_image (line 401) | fn test_change_to_previous_image() {
  function delete_current_file_deletes_file_from_filesystem (line 443) | fn delete_current_file_deletes_file_from_filesystem() {
  function delete_current_file_returns_deleted_file_path (line 458) | fn delete_current_file_returns_deleted_file_path() {
  function file_list_goes_to_next_file_after_removal_of_current_file (line 474) | fn file_list_goes_to_next_file_after_removal_of_current_file() {

FILE: src/image.rs
  type Coordinates (line 8) | pub type Coordinates = (u32, u32);
  type CoordinatesPair (line 9) | pub type CoordinatesPair = (Coordinates, Coordinates);
  type Image (line 11) | pub struct Image {
    method load (line 20) | pub fn load<P: AsRef<Path>>(path: P) -> Result<Image> {
    method save (line 31) | pub fn save<P: AsRef<Path>>(&mut self, path: P, clear_operations: bool...
    method reload (line 68) | pub fn reload<P: AsRef<Path>>(self, path: P) -> Result<Image> {
    method remove_image_buffers (line 89) | pub fn remove_image_buffers(&mut self) {
    method image_buffer_scale_to_fit (line 95) | fn image_buffer_scale_to_fit(&self, canvas_width: u32, canvas_height: ...
    method image_buffer_resize (line 112) | fn image_buffer_resize(&self, scale: u32) -> Option<Pixbuf> {
    method create_preview_image_buffer (line 124) | pub fn create_preview_image_buffer(&mut self, preview_size: PreviewSiz...
    method create_print_image_buffer (line 134) | pub fn create_print_image_buffer(
    method preview_image_buffer (line 150) | pub fn preview_image_buffer(&self) -> Option<&Pixbuf> {
    method current_image_buffer (line 154) | pub fn current_image_buffer(&self) -> Option<&Pixbuf> {
    method image_size (line 158) | pub fn image_size(&self) -> Option<(u32, u32)> {
    method image_aspect_ratio (line 164) | pub fn image_aspect_ratio(&self) -> Option<f64> {
    method preview_image_buffer_size (line 169) | pub fn preview_image_buffer_size(&self) -> Option<(u32, u32)> {
    method preview_coords_to_image_coords (line 175) | pub fn preview_coords_to_image_coords(
    method has_operations (line 201) | pub fn has_operations(&self) -> bool {
    method can_undo_operation (line 205) | pub fn can_undo_operation(&self) -> bool {
    method undo_operation (line 209) | pub fn undo_operation(&mut self) {
    method can_redo_operation (line 227) | pub fn can_redo_operation(&self) -> bool {
    method redo_operation (line 234) | pub fn redo_operation(&mut self) {
  type Result (line 255) | type Result = Self;
  method apply_operation (line 257) | fn apply_operation(mut self, image_operation: &ImageOperation) -> Self::...
  type PreviewSize (line 275) | pub enum PreviewSize {
    method smaller (line 292) | pub fn smaller(self) -> Option<PreviewSize> {
    method smaller_by (line 311) | pub fn smaller_by(self, value: u32) -> Option<PreviewSize> {
    method can_be_smaller (line 330) | pub fn can_be_smaller(&self) -> bool {
    method larger (line 334) | pub fn larger(self) -> Option<PreviewSize> {
    method larger_by (line 353) | pub fn larger_by(self, value: u32) -> Option<PreviewSize> {
    method can_be_larger (line 372) | pub fn can_be_larger(&self) -> bool {
  method from (line 282) | fn from(value: PreviewSize) -> Self {
  constant TEST_IMAGE (line 385) | const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");
  function test_load_image (line 388) | fn test_load_image() {
  function save_image (line 409) | fn save_image() {
  function test_save_image_without_clear_operations (line 420) | fn test_save_image_without_clear_operations() {
  function test_save_image_with_clear_operations (line 439) | fn test_save_image_with_clear_operations() {
  function save_image_uses_extensions_for_file_types_supported_by_pixbuf_save (line 457) | fn save_image_uses_extensions_for_file_types_supported_by_pixbuf_save() {
  function file_extensions_jpg_and_jpeg_are_supported (line 480) | fn file_extensions_jpg_and_jpeg_are_supported() {
  function test_image_reload (line 507) | fn test_image_reload() {
  function create_preview_original_size (line 532) | fn create_preview_original_size() {
  function create_preview_scale_to_fit (line 546) | fn create_preview_scale_to_fit() {
  function create_preview_resized (line 558) | fn create_preview_resized() {
  function preview_coords_to_image_coords (line 570) | fn preview_coords_to_image_coords() {
  function undo_operation (line 587) | fn undo_operation() {
  function redo_operation (line 611) | fn redo_operation() {
  function apply_operation (line 635) | fn apply_operation() {

FILE: src/image_list.rs
  type ImageList (line 12) | pub struct ImageList {
    method new (line 18) | pub fn new() -> Self {
    method remove (line 25) | pub fn remove(&mut self, key: &Path) -> Option<Image> {
    method insert (line 29) | pub fn insert(&mut self, key: PathBuf, value: Image) {
    method set_current_image_path (line 33) | pub fn set_current_image_path(&mut self, current_image_path: Option<Pa...
    method remove_current_image (line 41) | pub fn remove_current_image(&mut self) -> Option<Image> {
    method current_image_mut (line 47) | pub fn current_image_mut(&mut self) -> Option<&mut Image> {
    method current_image (line 53) | pub fn current_image(&self) -> Option<&Image> {
    method current_image_path (line 59) | pub fn current_image_path(&self) -> Option<PathBuf> {
    method save_current_image (line 63) | pub fn save_current_image(&mut self, filename: Option<PathBuf>) -> Res...
    method copy_current_image (line 83) | pub fn copy_current_image(&self, clipboard: gtk::gdk::Clipboard) {
    type Output (line 93) | type Output = Image;
    method index (line 95) | fn index(&self, index: &PathBuf) -> &Self::Output {
    method index_mut (line 101) | fn index_mut(&mut self, index: &PathBuf) -> &mut Self::Output {
  constant TEST_IMAGE (line 115) | const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");
  function save_current_image_overwrites_image_at_current_image_path_when_filename_is_set_to_none (line 118) | fn save_current_image_overwrites_image_at_current_image_path_when_filena...
  function save_current_image_creates_a_new_image_when_filename_is_set (line 148) | fn save_current_image_creates_a_new_image_when_filename_is_set() {
  function save_current_image_clears_image_operations_when_filename_is_set_to_none (line 169) | fn save_current_image_clears_image_operations_when_filename_is_set_to_no...
  function save_current_image_does_not_clear_image_operations_when_filename_is_set (line 192) | fn save_current_image_does_not_clear_image_operations_when_filename_is_s...

FILE: src/image_operation.rs
  type ImageOperation (line 8) | pub enum ImageOperation {
  type ApplyImageOperation (line 14) | pub trait ApplyImageOperation {
    method apply_operation (line 17) | fn apply_operation(self, image_operation: &ImageOperation) -> Self::Re...
    type Result (line 21) | type Result = Option<Pixbuf>;
    method apply_operation (line 23) | fn apply_operation(self, image_operation: &ImageOperation) -> Self::Re...
  constant TEST_IMAGE (line 49) | const TEST_IMAGE: &[u8] = include_bytes!("resources/test/test_image.png");
  function test_apply_rotate_image_operation_on_pixbuf (line 52) | fn test_apply_rotate_image_operation_on_pixbuf() {
  function test_apply_crop_image_operation_on_pixbuf (line 73) | fn test_apply_crop_image_operation_on_pixbuf() {
  function test_apply_resize_image_operation_on_pixbuf (line 91) | fn test_apply_resize_image_operation_on_pixbuf() {

FILE: src/main.rs
  function main (line 18) | fn main() {

FILE: src/settings.rs
  type Settings (line 8) | pub struct Settings {
    method new (line 16) | pub fn new(application_id: &str) -> Settings {
    method set_window_size (line 36) | pub fn set_window_size(&self, window_size: (u32, u32)) {
    method window_size (line 48) | pub fn window_size(&self) -> (u32, u32) {
    method set_scale (line 58) | pub fn set_scale(&mut self, preview_size: PreviewSize) {
    method scale (line 62) | pub fn scale(&self) -> PreviewSize {
    method set_fullscreen (line 66) | pub fn set_fullscreen(&mut self, fullscreen: bool) {
    method fullscreen (line 70) | pub fn fullscreen(&self) -> bool {
    method scale_before_zoom_gesture (line 74) | pub fn scale_before_zoom_gesture(&self) -> Option<PreviewSize> {
    method set_scale_before_zoom_gesture (line 78) | pub fn set_scale_before_zoom_gesture(

FILE: src/test_utils.rs
  type TestResources (line 3) | pub struct TestResources {
    method new (line 8) | pub fn new<P: AsRef<Path>>(file_folder: P) -> Self {
    method add_file (line 15) | pub fn add_file<T: AsRef<str>, C: AsRef<[u8]>>(&mut self, file_name: T...
    method remove_file (line 19) | pub fn remove_file<T: AsRef<str>>(&mut self, file_name: T) {
    method file_folder (line 23) | pub fn file_folder(&self) -> &Path {
  method drop (line 29) | fn drop(&mut self) {

FILE: src/ui/action.rs
  function refresh_file_list (line 36) | pub fn refresh_file_list(sender: &Sender<Event>, file_list: &mut FileLis...
  function open_file (line 49) | pub fn open_file(
  function load_image (line 82) | pub fn load_image(
  function next_image (line 131) | pub fn next_image(
  function previous_image (line 143) | pub fn previous_image(
  function image_viewport_resize (line 155) | pub fn image_viewport_resize(
  function refresh_preview (line 167) | pub fn refresh_preview(
  function change_preview_size (line 193) | pub fn change_preview_size(
  function preview_smaller (line 210) | pub fn preview_smaller(sender: &Sender<Event>, settings: &Settings, valu...
  function preview_larger (line 220) | pub fn preview_larger(sender: &Sender<Event>, settings: &Settings, value...
  function preview_fit_screen (line 230) | pub fn preview_fit_screen(sender: &Sender<Event>) {
  function image_edit (line 235) | pub fn image_edit(
  function start_selection (line 250) | pub fn start_selection(
  function drag_selection (line 262) | pub fn drag_selection(
  function end_selection (line 281) | pub fn end_selection(
  function resize_popover_displayed (line 302) | pub fn resize_popover_displayed(widgets: &Widgets, image_list: Rc<RefCel...
  function update_resize_popover_width (line 310) | pub fn update_resize_popover_width(widgets: &Widgets, image_list: Rc<Ref...
  function update_resize_popover_height (line 319) | pub fn update_resize_popover_height(widgets: &Widgets, image_list: Rc<Re...
  function save_current_image (line 328) | pub fn save_current_image(
  function delete_current_image (line 341) | pub fn delete_current_image(
  function print (line 370) | pub fn print(sender: &Sender<Event>, widgets: &Widgets, image_list: Rc<R...
  function undo_operation (line 423) | pub fn undo_operation(
  function redo_operation (line 434) | pub fn redo_operation(
  function display_message (line 445) | pub fn display_message(widgets: &Widgets, message: &str, message_type: g...
  function hide_info_panel (line 463) | pub fn hide_info_panel(widgets: &Widgets) {
  function toggle_fullscreen (line 471) | pub fn toggle_fullscreen(widgets: &Widgets, settings: &mut Settings) {
  function quit (line 481) | pub fn quit(application: &gtk::Application) {
  function set_as_wallpaper (line 489) | pub fn set_as_wallpaper(sender: &Sender<Event>, file_list: &FileList) {
  function set_as_wallpaper (line 511) | pub fn set_as_wallpaper(_sender: &Sender<Event>, _file_list: &FileList) {
  function copy_current_image (line 515) | pub fn copy_current_image(image_list: Rc<RefCell<ImageList>>) {
  function start_zoom_gesture (line 520) | pub fn start_zoom_gesture(settings: &mut Settings) {
  function change_scale_on_zoom_gesture (line 524) | pub fn change_scale_on_zoom_gesture(sender: &Sender<Event>, settings: &S...
  function update_buttons_state (line 538) | pub fn update_buttons_state(

FILE: src/ui/controllers.rs
  type Controllers (line 4) | pub struct Controllers {
    method init (line 13) | pub fn init() -> Self {
    method image_click_gesture (line 25) | pub fn image_click_gesture(&self) -> &gtk::GestureClick {
    method image_motion_event_controller (line 29) | pub fn image_motion_event_controller(&self) -> &gtk::EventControllerMo...
    method image_zoom_gesture (line 33) | pub fn image_zoom_gesture(&self) -> &gtk::GestureZoom {
    method window_key_event_controller (line 37) | pub fn window_key_event_controller(&self) -> &gtk::EventControllerKey {
    method image_scrolled_window_scroll_controller (line 41) | pub fn image_scrolled_window_scroll_controller(&self) -> &gtk::EventCo...

FILE: src/ui/event.rs
  type Event (line 30) | pub enum Event {
  function post_event (line 64) | pub fn post_event(sender: &glib::Sender<Event>, action: Event) {
  function connect_events (line 70) | pub fn connect_events(
  function connect_controllers (line 107) | pub fn connect_controllers(sender: Sender<Event>, widgets: Widgets, cont...
  function connect_controllers_to_widgets (line 121) | fn connect_controllers_to_widgets(widgets: Widgets, controllers: Control...
  function connect_keybinds (line 139) | pub fn connect_keybinds(controllers: Controllers, widgets: Widgets, send...
  function connect_open_menu_button_clicked (line 253) | fn connect_open_menu_button_clicked(widgets: Widgets, sender: Sender<Eve...
  function connect_next_button_clicked (line 299) | fn connect_next_button_clicked(widgets: Widgets, sender: Sender<Event>) {
  function connect_previous_button_clicked (line 305) | fn connect_previous_button_clicked(widgets: Widgets, sender: Sender<Even...
  function connect_window_default_width_notify (line 311) | fn connect_window_default_width_notify(
  function connect_window_default_height_notify (line 331) | fn connect_window_default_height_notify(
  function connect_window_fullscreened_notify (line 351) | fn connect_window_fullscreened_notify(widgets: Widgets, sender: Sender<E...
  function connect_window_maximized_notify (line 372) | fn connect_window_maximized_notify(widgets: Widgets, sender: Sender<Even...
  function connect_preview_smaller_button_clicked (line 390) | fn connect_preview_smaller_button_clicked(widgets: Widgets, sender: Send...
  function connect_preview_larger_button_clicked (line 396) | fn connect_preview_larger_button_clicked(widgets: Widgets, sender: Sende...
  function connect_preview_fit_screen_button_clicked (line 402) | fn connect_preview_fit_screen_button_clicked(widgets: Widgets, sender: S...
  function connect_rotate_counterclockwise_button_clicked (line 410) | fn connect_rotate_counterclockwise_button_clicked(widgets: Widgets, send...
  function connect_rotate_clockwise_button_clicked (line 421) | fn connect_rotate_clockwise_button_clicked(widgets: Widgets, sender: Sen...
  function connect_image_click_pressed_gesture (line 430) | fn connect_image_click_pressed_gesture(controllers: Controllers, sender:...
  function connect_image_motion_event_controller_motion (line 438) | fn connect_image_motion_event_controller_motion(controllers: Controllers...
  function connect_image_click_released_gesture (line 446) | fn connect_image_click_released_gesture(controllers: Controllers, sender...
  function connect_image_widget_draw (line 454) | fn connect_image_widget_draw(
  function connect_resize_button_activated (line 491) | fn connect_resize_button_activated(widgets: Widgets, sender: Sender<Even...
  function connect_width_spin_button_value_changed (line 497) | fn connect_width_spin_button_value_changed(widgets: Widgets, sender: Sen...
  function connect_height_spin_button_value_changed (line 508) | fn connect_height_spin_button_value_changed(widgets: Widgets, sender: Se...
  function connect_apply_resize_button_clicked (line 519) | fn connect_apply_resize_button_clicked(widgets: Widgets, sender: Sender<...
  function connect_save_menu_button_clicked (line 535) | fn connect_save_menu_button_clicked(widgets: Widgets, sender: Sender<Eve...
  function connect_save_as_menu_button_clicked (line 545) | fn connect_save_as_menu_button_clicked(
  function connect_print_menu_button_clicked (line 603) | fn connect_print_menu_button_clicked(widgets: Widgets, sender: Sender<Ev...
  function connect_undo_button_clicked (line 614) | fn connect_undo_button_clicked(widgets: Widgets, sender: Sender<Event>) {
  function connect_redo_button_clicked (line 620) | fn connect_redo_button_clicked(widgets: Widgets, sender: Sender<Event>) {
  function connect_delete_button_clicked (line 626) | fn connect_delete_button_clicked(widgets: Widgets, sender: Sender<Event>) {
  function connect_info_bar_response (line 632) | fn connect_info_bar_response(widgets: Widgets) {
  function connect_image_scrolled_window_scroll_controller_scroll (line 640) | fn connect_image_scrolled_window_scroll_controller_scroll(
  function connect_set_as_wallpaper_menu_button_clicked (line 658) | fn connect_set_as_wallpaper_menu_button_clicked(widgets: Widgets, sender...
  function connect_copy_menu_button_clicked (line 666) | fn connect_copy_menu_button_clicked(widgets: Widgets, sender: Sender<Eve...
  function connect_zoom_gesture_begin (line 676) | fn connect_zoom_gesture_begin(controllers: Controllers, sender: Sender<E...
  function connect_zoom_gesture_scale_changed (line 682) | fn connect_zoom_gesture_scale_changed(controllers: Controllers, sender: ...

FILE: src/ui/widgets.rs
  type Widgets (line 9) | pub struct Widgets {
    method init (line 44) | pub fn init(builder: Builder, application: &gtk::Application) -> Self {
    method window (line 201) | pub fn window(&self) -> &ApplicationWindow {
    method open_menu_button (line 206) | pub fn open_menu_button(&self) -> &gtk::Button {
    method image_widget (line 211) | pub fn image_widget(&self) -> &gtk::DrawingArea {
    method popover_menu (line 216) | pub fn popover_menu(&self) -> &gtk::PopoverMenu {
    method next_button (line 221) | pub fn next_button(&self) -> &gtk::Button {
    method previous_button (line 226) | pub fn previous_button(&self) -> &gtk::Button {
    method preview_smaller_button (line 231) | pub fn preview_smaller_button(&self) -> &gtk::Button {
    method preview_larger_button (line 236) | pub fn preview_larger_button(&self) -> &gtk::Button {
    method image_viewport (line 241) | pub fn image_viewport(&self) -> &gtk::Viewport {
    method rotate_counterclockwise_button (line 246) | pub fn rotate_counterclockwise_button(&self) -> &gtk::Button {
    method rotate_clockwise_button (line 251) | pub fn rotate_clockwise_button(&self) -> &gtk::Button {
    method crop_button (line 256) | pub fn crop_button(&self) -> &gtk::ToggleButton {
    method resize_button (line 261) | pub fn resize_button(&self) -> &gtk::MenuButton {
    method width_spin_button (line 266) | pub fn width_spin_button(&self) -> &gtk::SpinButton {
    method height_spin_button (line 271) | pub fn height_spin_button(&self) -> &gtk::SpinButton {
    method link_aspect_ratio_button (line 276) | pub fn link_aspect_ratio_button(&self) -> &gtk::ToggleButton {
    method apply_resize_button (line 281) | pub fn apply_resize_button(&self) -> &gtk::Button {
    method info_bar (line 286) | pub fn info_bar(&self) -> &gtk::InfoBar {
    method info_bar_text (line 291) | pub fn info_bar_text(&self) -> &gtk::Label {
    method save_menu_button (line 296) | pub fn save_menu_button(&self) -> &gtk::Button {
    method print_menu_button (line 301) | pub fn print_menu_button(&self) -> &gtk::Button {
    method undo_button (line 306) | pub fn undo_button(&self) -> &gtk::Button {
    method redo_button (line 311) | pub fn redo_button(&self) -> &gtk::Button {
    method save_as_menu_button (line 316) | pub fn save_as_menu_button(&self) -> &gtk::Button {
    method preview_fit_screen_button (line 321) | pub fn preview_fit_screen_button(&self) -> &gtk::Button {
    method delete_button (line 326) | pub fn delete_button(&self) -> &gtk::Button {
    method preview_size_label (line 331) | pub fn preview_size_label(&self) -> &gtk::Label {
    method image_scrolled_window (line 336) | pub fn image_scrolled_window(&self) -> &gtk::ScrolledWindow {
    method set_as_wallpaper_menu_button (line 341) | pub fn set_as_wallpaper_menu_button(&self) -> &gtk::Button {
    method copy_menu_button (line 346) | pub fn copy_menu_button(&self) -> &gtk::Button {
    method file_chooser (line 350) | pub fn file_chooser(&self) -> &RefCell<Option<gtk::FileChooserNative>> {
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (246K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 3793,
    "preview": "on: [push, pull_request]\n\nname: Continuous integration\n\njobs:\n  check:\n    name: Check\n    runs-on: ubuntu-22.04\n    ste"
  },
  {
    "path": ".gitignore",
    "chars": 72,
    "preview": "/target\n/build-dir\nsrc/resources/image-roll_ui.glade~\n/.flatpak-builder\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 1491,
    "preview": "[package]\nname = \"image-roll\"\nversion = \"2.1.0\"\nlicense = \"MIT\"\ndescription = \"Image Roll is a simple and fast GTK image"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2021 Robert Węcławski\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 1609,
    "preview": "\n# Image Roll\n![Image Roll](https://raw.githubusercontent.com/weclaw1/image-roll/main/src/resources/com.github.weclaw1.I"
  },
  {
    "path": "build.rs",
    "chars": 2401,
    "preview": "use std::{env, process::Command};\n\nfn main() {\n    Command::new(\"glib-compile-resources\")\n        .args(&[\"src/resources"
  },
  {
    "path": "debian/postinst",
    "chars": 72,
    "preview": "#!/bin/sh\nset -e\nglib-compile-schemas /usr/share/glib-2.0/schemas\nexit 0"
  },
  {
    "path": "src/app.rs",
    "chars": 9023,
    "preview": "use gtk::{\n    gdk::Display,\n    gio,\n    glib::{self, timeout_future},\n    prelude::*,\n    Builder,\n};\n\nuse std::{\n    "
  },
  {
    "path": "src/file_list.rs",
    "chars": 16523,
    "preview": "use std::path::PathBuf;\n\nuse anyhow::{anyhow, Result};\nuse gtk::{\n    gio::{self, Cancellable, FileMonitorFlags, FileQue"
  },
  {
    "path": "src/image.rs",
    "chars": 25378,
    "preview": "use std::path::Path;\n\nuse anyhow::{anyhow, Result};\nuse gtk::gdk_pixbuf::{InterpType, Pixbuf};\n\nuse crate::image_operati"
  },
  {
    "path": "src/image_list.rs",
    "chars": 6883,
    "preview": "use std::{\n    collections::HashMap,\n    ops::{Index, IndexMut},\n    path::{Path, PathBuf},\n};\n\nuse crate::image::Image;"
  },
  {
    "path": "src/image_operation.rs",
    "chars": 3615,
    "preview": "use std::cmp;\n\nuse gtk::gdk_pixbuf::{InterpType, Pixbuf, PixbufRotation};\n\nuse crate::image::CoordinatesPair;\n\n#[derive("
  },
  {
    "path": "src/main.rs",
    "chars": 644,
    "preview": "use app::App;\nuse gtk::{gio::ApplicationFlags, prelude::*, Application};\n\n#[macro_use]\nextern crate log;\n\nmod app;\nmod f"
  },
  {
    "path": "src/resources/cargo-sources.json",
    "chars": 73643,
    "preview": "[\n    {\n        \"type\": \"archive\",\n        \"archive-type\": \"tar-gzip\",\n        \"url\": \"https://static.crates.io/crates/a"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.desktop",
    "chars": 636,
    "preview": "[Desktop Entry]\nType=Application\nName=Image Roll\nComment=Image viewer with basic image manipulation tools\nExec=image-rol"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.gschema.xml",
    "chars": 408,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<schemalist>\n  <schema id=\"com.github.weclaw1.ImageRoll\" path=\"/com/github/weclaw"
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.metainfo.xml",
    "chars": 1053,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop-application\">\n  <id>com.github.weclaw1.ImageRoll</id>\n  "
  },
  {
    "path": "src/resources/com.github.weclaw1.ImageRoll.yaml",
    "chars": 1273,
    "preview": "app-id: com.github.weclaw1.ImageRoll\nruntime: org.gnome.Platform\nruntime-version: '42'\nsdk: org.gnome.Sdk\nsdk-extensions"
  },
  {
    "path": "src/resources/image-roll.cmb",
    "chars": 8765,
    "preview": "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n<!DOCTYPE cambalache-project SYSTEM \"cambalache-project.dtd\">\n<ca"
  },
  {
    "path": "src/resources/image-roll.ui",
    "chars": 9960,
    "preview": "<?xml version='1.0' encoding='UTF-8'?>\n<!-- Created with Cambalache 0.9.1 -->\n<interface>\n  <!-- interface-name image-ro"
  },
  {
    "path": "src/resources/resources.xml",
    "chars": 405,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gresources>\n  <gresource prefix=\"/com/github/weclaw1/image-roll\">\n    <file>imag"
  },
  {
    "path": "src/settings.rs",
    "chars": 2359,
    "preview": "use gtk::gio;\nuse gtk::gio::prelude::SettingsExt;\nuse gtk::gio::SettingsSchemaSource;\n\nuse crate::image::PreviewSize;\n\n#"
  },
  {
    "path": "src/test_utils.rs",
    "chars": 872,
    "preview": "use std::path::{Path, PathBuf};\n\npub struct TestResources {\n    file_folder: PathBuf,\n}\n\nimpl TestResources {\n    pub fn"
  },
  {
    "path": "src/ui/action.rs",
    "chars": 19487,
    "preview": "use std::{\n    cell::{Cell, RefCell},\n    path::PathBuf,\n    rc::Rc,\n};\n\n#[cfg(feature = \"wallpaper\")]\nuse ashpd::{\n    "
  },
  {
    "path": "src/ui/controllers.rs",
    "chars": 1478,
    "preview": "use gtk::EventControllerScrollFlags;\n\n#[derive(Clone)]\npub struct Controllers {\n    window_key_event_controller: gtk::Ev"
  },
  {
    "path": "src/ui/event.rs",
    "chars": 25298,
    "preview": "use gtk::{\n    gdk::{self, Key},\n    gdk_pixbuf::PixbufRotation,\n    gio,\n    glib::{self, timeout_future, Sender},\n    "
  },
  {
    "path": "src/ui/widgets.rs",
    "chars": 11904,
    "preview": "use std::cell::RefCell;\n\nuse gtk::{\n    prelude::{GtkWindowExt, WidgetExt},\n    ApplicationWindow, Builder,\n};\n\n#[derive"
  },
  {
    "path": "src/ui.rs",
    "chars": 69,
    "preview": "pub mod action;\npub mod controllers;\npub mod event;\npub mod widgets;\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the weclaw1/image-roll GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (224.8 KB), approximately 62.4k tokens, and a symbol index with 232 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!