├── debian ├── compat ├── source │ └── format ├── popsicle.install ├── popsicle-gtk.install ├── copyright ├── rules ├── control └── changelog ├── rust-toolchain ├── cli ├── i18n.toml ├── Cargo.toml └── src │ ├── localize.rs │ └── main.rs ├── gtk ├── i18n.toml ├── src │ ├── app │ │ ├── widgets │ │ │ ├── mod.rs │ │ │ ├── header.rs │ │ │ └── dialogs.rs │ │ ├── ui.css │ │ ├── views │ │ │ ├── error.rs │ │ │ ├── summary.rs │ │ │ ├── flashing.rs │ │ │ ├── mod.rs │ │ │ ├── view.rs │ │ │ ├── devices.rs │ │ │ └── images.rs │ │ ├── signals │ │ │ ├── devices.rs │ │ │ ├── images.rs │ │ │ └── mod.rs │ │ ├── state │ │ │ └── mod.rs │ │ ├── events │ │ │ └── mod.rs │ │ └── mod.rs │ ├── gresource.rs │ ├── hash.rs │ ├── localize.rs │ ├── main.rs │ ├── misc.rs │ └── flash.rs ├── assets │ ├── application-x-cd-image.png │ ├── drive-removable-media-usb.png │ ├── icons │ │ ├── 16x16 │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 24x24 │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 48x48 │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 16x16@2x │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 24x24@2x │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 32x32@2x │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 48x48@2x │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ ├── 512x512 │ │ │ └── apps │ │ │ │ └── com.system76.Popsicle.png │ │ └── 512x512@2x │ │ │ └── apps │ │ │ └── com.system76.Popsicle.png │ ├── resources.gresource.xml │ ├── com.system76.Popsicle.desktop │ ├── com.system76.Popsicle.appdata.xml │ └── process-completed-symbolic.svg ├── build.rs └── Cargo.toml ├── screenshots ├── screenshot-01.png ├── screenshot-02.png ├── screenshot-03.png ├── screenshot-04.png ├── screenshot-05.png └── device-monitoring.gif ├── CODE_OF_CONDUCT.md ├── rustfmt.toml ├── .gitignore ├── appimage.sh ├── tests ├── ipc.ron └── ipc.rs ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── i18n ├── zh-CN │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── ja │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── ko │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── he │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── en │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── da │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── bn │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── sv │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── ru │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── cs │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── sl │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── sq │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── hi │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── sk │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── fi │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── pt │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── hu │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── it │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── ca │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── pt-BR │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── nl │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── bg │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── de │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── sr │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── tr │ ├── popsicle_cli.ftl │ └── popsicle_gtk.ftl ├── pl │ └── popsicle_gtk.ftl ├── fr │ └── popsicle_gtk.ftl └── es │ └── popsicle_gtk.ftl ├── Cargo.toml ├── appimagecraft.yml ├── LICENSE ├── com.system76.Popsicle.json ├── src ├── codec.rs ├── task.rs └── lib.rs ├── README.md └── Makefile /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.70.0 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /debian/popsicle.install: -------------------------------------------------------------------------------- 1 | usr/bin/popsicle 2 | usr/share/man/man1 3 | -------------------------------------------------------------------------------- /cli/i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "../i18n" 5 | -------------------------------------------------------------------------------- /gtk/i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "../i18n" 5 | -------------------------------------------------------------------------------- /screenshots/screenshot-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/screenshots/screenshot-01.png -------------------------------------------------------------------------------- /screenshots/screenshot-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/screenshots/screenshot-02.png -------------------------------------------------------------------------------- /screenshots/screenshot-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/screenshots/screenshot-03.png -------------------------------------------------------------------------------- /screenshots/screenshot-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/screenshots/screenshot-04.png -------------------------------------------------------------------------------- /screenshots/screenshot-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/screenshots/screenshot-05.png -------------------------------------------------------------------------------- /screenshots/device-monitoring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/screenshots/device-monitoring.gif -------------------------------------------------------------------------------- /gtk/src/app/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod dialogs; 2 | mod header; 3 | 4 | pub use self::dialogs::*; 5 | pub use self::header::*; 6 | -------------------------------------------------------------------------------- /gtk/assets/application-x-cd-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/application-x-cd-image.png -------------------------------------------------------------------------------- /gtk/assets/drive-removable-media-usb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/drive-removable-media-usb.png -------------------------------------------------------------------------------- /gtk/src/app/ui.css: -------------------------------------------------------------------------------- 1 | .h2 { 2 | font-size: 1.25em; 3 | font-weight: bold; 4 | } 5 | 6 | .bold { 7 | font-weight: bold; 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributors to this repo agree to be bound by the [Pop! Code of Conduct](https://github.com/pop-os/code-of-conduct). 2 | -------------------------------------------------------------------------------- /gtk/assets/icons/16x16/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/16x16/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/24x24/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/24x24/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/32x32/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/32x32/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/48x48/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/48x48/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/16x16@2x/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/16x16@2x/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/24x24@2x/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/24x24@2x/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/32x32@2x/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/32x32@2x/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/48x48@2x/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/48x48@2x/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /gtk/assets/icons/512x512/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/512x512/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | reorder_imports = true 3 | reorder_modules = true 4 | use_field_init_shorthand = true 5 | use_small_heuristics = "Max" 6 | 7 | -------------------------------------------------------------------------------- /gtk/assets/icons/512x512@2x/apps/com.system76.Popsicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/popsicle/HEAD/gtk/assets/icons/512x512@2x/apps/com.system76.Popsicle.png -------------------------------------------------------------------------------- /debian/popsicle-gtk.install: -------------------------------------------------------------------------------- 1 | usr/bin/popsicle-gtk 2 | usr/share/applications/com.system76.Popsicle.desktop 3 | usr/share/metainfo/com.system76.Popsicle.appdata.xml 4 | usr/share/icons/hicolor/ 5 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: popsicle 3 | Source: https://github.com/pop-os/popsicle 4 | 5 | Files: * 6 | Copyright: Copyright 2017 System76 7 | License: MIT 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cargo/ 2 | debian/* 3 | !debian/source 4 | !debian/changelog 5 | !debian/compat 6 | !debian/control 7 | !debian/copyright 8 | !debian/*.install 9 | !debian/rules 10 | target/ 11 | vendor/ 12 | vendor.tar 13 | *.AppImage* 14 | *.zsync 15 | *.swp 16 | .appimagecraft-*/ 17 | -------------------------------------------------------------------------------- /gtk/src/app/views/error.rs: -------------------------------------------------------------------------------- 1 | use super::View; 2 | use crate::fl; 3 | 4 | pub struct ErrorView { 5 | pub view: View, 6 | } 7 | 8 | impl ErrorView { 9 | pub fn new() -> ErrorView { 10 | ErrorView { view: View::new("dialog-error", &fl!("critical-error"), "", |_| ()) } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gtk/src/app/signals/devices.rs: -------------------------------------------------------------------------------- 1 | use crate::app::App; 2 | use gtk::prelude::*; 3 | 4 | impl App { 5 | pub fn connect_view_ready(&self) { 6 | let next = self.ui.header.next.clone(); 7 | self.ui.content.devices_view.connect_view_ready(move |ready| next.set_sensitive(ready)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gtk/assets/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | application-x-cd-image.png 5 | drive-removable-media-usb.png 6 | process-completed-symbolic.svg 7 | 8 | 9 | -------------------------------------------------------------------------------- /appimage.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | export APPIMAGE_EXTRACT_AND_RUN=1 4 | git config --global --add safe.directory /github/workspace 5 | apt-get update 6 | apt-get install -y help2man libclang-dev libgtk-3-dev patchelf 7 | wget https://github.com/TheAssassin/appimagecraft/releases/download/continuous/appimagecraft-x86_64.AppImage 8 | chmod +x appimagecraft-x86_64.AppImage 9 | ./appimagecraft-x86_64.AppImage 10 | -------------------------------------------------------------------------------- /gtk/src/gresource.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | 3 | pub fn init() -> Result<(), glib::Error> { 4 | const GRESOURCE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/compiled.gresource")); 5 | 6 | gio::resources_register(&gio::Resource::from_data(&glib::Bytes::from_static(GRESOURCE))?); 7 | 8 | let theme = gtk::IconTheme::default().unwrap(); 9 | theme.add_resource_path("/org/Pop-OS/Popsicle"); 10 | 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export VENDORED ?= 1 4 | CLEAN ?= 1 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_auto_build: 10 | env CARGO_HOME="$$(pwd)/target/cargo" \ 11 | dh_auto_build 12 | 13 | override_dh_auto_clean: 14 | ifeq ($(CLEAN),1) 15 | make clean 16 | endif 17 | ifeq ($(VENDORED),1) 18 | if ! ischroot; then \ 19 | make vendor; \ 20 | fi 21 | endif 22 | 23 | override_dh_auto_install: 24 | dh_auto_install -- prefix=/usr 25 | -------------------------------------------------------------------------------- /tests/ipc.ron: -------------------------------------------------------------------------------- 1 | Size(2229190656) 2 | Device("/dev/sdb") 3 | Device("/dev/sda") 4 | Set("/dev/sda",589824) 5 | Set("/dev/sdb",589824) 6 | Set("/dev/sdb",384434176) 7 | Set("/dev/sda",1669005312) 8 | Set("/dev/sdb",2228748288) 9 | Set("/dev/sda",0) 10 | Message("/dev/sda","S") 11 | Set("/dev/sdb",0) 12 | Message("/dev/sdb","S") 13 | Set("/dev/sda",0) 14 | Message("/dev/sda","V") 15 | Set("/dev/sdb",0) 16 | Message("/dev/sdb","V") 17 | Finished("/dev/sda") 18 | Finished("/dev/sdb") 19 | -------------------------------------------------------------------------------- /gtk/src/app/views/summary.rs: -------------------------------------------------------------------------------- 1 | use super::View; 2 | use crate::fl; 3 | use gtk::{prelude::*, *}; 4 | 5 | pub struct SummaryView { 6 | pub view: View, 7 | pub list: ListBox, 8 | } 9 | 10 | impl SummaryView { 11 | pub fn new() -> SummaryView { 12 | let list = cascade! { 13 | ListBox::new(); 14 | ..style_context().add_class("frame"); 15 | }; 16 | 17 | let view = View::new("process-completed", &fl!("flashing-completed"), "", |right_panel| { 18 | right_panel.pack_start(&list, true, true, 0); 19 | }); 20 | 21 | SummaryView { view, list } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: popsicle 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Jeremy Soller 5 | Build-Depends: 6 | debhelper (>=9), 7 | cargo, 8 | help2man, 9 | libclang-dev, 10 | libgtk-3-dev 11 | Standards-Version: 4.1.1 12 | Homepage: https://github.com/pop-os/popsicle 13 | 14 | Package: popsicle 15 | Architecture: amd64 arm64 16 | Depends: 17 | ${misc:Depends}, 18 | ${shlib:Depends} 19 | Description: USB Flasher 20 | 21 | Package: popsicle-gtk 22 | Architecture: amd64 arm64 23 | Depends: 24 | libgtk-3-0, 25 | ${misc:Depends}, 26 | ${shlib:Depends} 27 | Description: GTK front end to the USB Flasher 28 | -------------------------------------------------------------------------------- /gtk/src/hash.rs: -------------------------------------------------------------------------------- 1 | use digest::Digest; 2 | use hex_view::HexView; 3 | use std::fs::File; 4 | use std::io::{self, Read}; 5 | use std::path::Path; 6 | 7 | pub(crate) fn hasher(image: &Path) -> io::Result { 8 | File::open(image).and_then(move |mut file| { 9 | let mut buffer = [0u8; 8 * 1024]; 10 | let mut hasher = H::new(); 11 | 12 | loop { 13 | let read = file.read(&mut buffer)?; 14 | if read == 0 { 15 | break; 16 | } 17 | hasher.update(&buffer[..read]); 18 | } 19 | 20 | Ok(format!("{:x}", HexView::from(hasher.finalize().as_slice()))) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | **Distribution (run `cat /etc/os-release`):** 9 | 10 | 11 | 12 | **Related Application and/or Package Version (run `apt policy $PACKAGE NAME`):** 13 | 14 | 15 | 16 | **Issue/Bug Description:** 17 | 18 | 19 | 20 | **Steps to reproduce (if you know):** 21 | 22 | 23 | 24 | **Expected behavior:** 25 | 26 | 27 | 28 | **Other Notes:** 29 | 30 | 31 | -------------------------------------------------------------------------------- /gtk/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, fs, 3 | process::{self, Command}, 4 | }; 5 | 6 | fn main() { 7 | for i in fs::read_dir("assets").unwrap() { 8 | println!("cargo:rerun-if-changed={}", i.unwrap().path().display()); 9 | } 10 | 11 | let out_dir = env::var("OUT_DIR").unwrap(); 12 | 13 | let status = Command::new("glib-compile-resources") 14 | .arg("--sourcedir=assets") 15 | .arg(format!("--target={}/compiled.gresource", out_dir)) 16 | .arg("assets/resources.gresource.xml") 17 | .status() 18 | .unwrap(); 19 | 20 | if !status.success() { 21 | eprintln!("glib-compile-resources failed with exit status {}", status); 22 | process::exit(1); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /i18n/zh-CN/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = 你确实要将 '{$image_path}' 镜像刷入到所示的磁盘吗? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = 镜像 8 | arg-image-desc = 要刷入的镜像文件 9 | 10 | arg-disks = 磁盘 11 | arg-disks-desc = 要输出到的磁盘 12 | 13 | arg-all-desc = 刷入所有检测到的 USB 设备 14 | arg-check-desc = 检测写入的镜像是否与源镜像一致 15 | arg-unmount-desc = 卸载已挂载的设备 16 | arg-yes-desc = 继续且无需确认 17 | 18 | # errors 19 | error-caused-by = 导致的原因 20 | error-image-not-set = {arg-image} 未设置 21 | error-image-open = 无法打开下列镜像:'{$image_path}' 22 | error-image-metadata = 无法获取下列镜像的元数据:'{$image_path}' 23 | error-disks-fetch = 无法获取 USB 磁盘列表 24 | error-no-disks-specified = 未设定磁盘 25 | error-fetching-mounts = 无法获取挂载项列表 26 | error-opening-disks = 无法打开磁盘 27 | error-exiting = 退出但不刷入 28 | error-reading-mounts = 无法读取挂载项 29 | -------------------------------------------------------------------------------- /gtk/assets/com.system76.Popsicle.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=Popsicle USB Flasher 4 | Name[fr_FR]=Flasheur USB Popsicle 5 | Name[pt_BR]=Gravador USB Popsicle 6 | GenericName=USB Flasher 7 | GenericName[fr_FR]=Flasheur USB 8 | GenericName[pt_BR]=Gravador USB 9 | X-GNOME-FullName=Popsicle USB Flasher 10 | X-GNOME-FullName[fr_FR]=Flasheur USB Popsicle 11 | X-GNOME-FullName[pt_BR]=Gravador USB Popsicle 12 | Comment=Multi-USB image flashing utility 13 | Icon=com.system76.Popsicle 14 | Categories=System; 15 | Keywords="USB;Flash;Drive;Popsicle;" 16 | Keywords[fr_FR]="USB;Flasheur;Lecteur;Popsicle;" 17 | Keywords[pt_BR]="USB;Gravar;Pendrive;Popsicle;" 18 | MimeType=application/x-cd-image;application/x-raw-disk-image; 19 | Terminal=false 20 | StartupNotify=true 21 | Exec=popsicle-gtk %f 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "popsicle" 3 | description = "USB Flasher" 4 | version = "1.3.3" 5 | authors = [ 6 | "Jeremy Soller ", 7 | "Michael Aaron Murphy ", 8 | ] 9 | license = "MIT" 10 | readme = "README.md" 11 | edition = "2021" 12 | rust-version = "1.70.0" 13 | 14 | [lib] 15 | name = "popsicle" 16 | path = "src/lib.rs" 17 | 18 | [workspace] 19 | members = ["cli", "gtk"] 20 | 21 | [dependencies] 22 | anyhow = "1.0.79" 23 | as-result = "0.2.1" 24 | async-std = "1.12.0" 25 | derive-new = "0.6.0" 26 | futures = "0.3.30" 27 | futures_codec = "0.4.1" 28 | libc = "0.2.151" 29 | memchr = "2.7.1" 30 | mnt = "0.3.1" 31 | ron = "0.8.1" 32 | serde = { version = "1.0.194", features = ["derive"] } 33 | srmw = "0.1.1" 34 | thiserror = "1.0.56" 35 | usb-disk-probe = "0.2.0" 36 | 37 | -------------------------------------------------------------------------------- /appimagecraft.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | 3 | project: 4 | name: com.github.pop-os.popsicle 5 | version_command: git describe --tags 6 | 7 | build: 8 | script: 9 | commands: 10 | # unfortunately, no out-of-source builds are possible (yet) 11 | - pushd "$PROJECT_ROOT" 12 | - make install DESTDIR="$(readlink -f "$BUILD_DIR"/AppDir)" prefix=/usr 13 | - popd 14 | 15 | scripts: 16 | post_build: 17 | # just USB Flasher might make sense on Pop!_OS, but not on other distros 18 | - sed -i 's|Name=USB Flasher|Name=Popsicle USB Flasher|' "$BUILD_DIR"/AppDir/usr/share/applications/com.system76.Popsicle.desktop 19 | 20 | appimage: 21 | linuxdeploy: 22 | environment: 23 | UPD_INFO: 'gh-releases-zsync|pop-os|popsicle|latest|Popsicle_USB_Flasher-*x86_64.AppImage.zsync' 24 | plugins: 25 | - gtk 26 | -------------------------------------------------------------------------------- /i18n/ja/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = 次のデバイスに'{$image_path}'を書き込むで間違いありませんか? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = イメージ 8 | arg-image-desc = 入力のイメージファイル 9 | 10 | arg-disks = デバイス 11 | arg-disks-desc = 出力のデバイス 12 | 13 | arg-all-desc = 認識されたすべてのデバイスに書き込む 14 | arg-check-desc = 書き込まれたイメージが入力と等しいか確認をする 15 | arg-unmount-desc = マウントされたデバイスをアンマウントする 16 | arg-yes-desc = 確認せずに続行する 17 | 18 | # errors 19 | error-caused-by = 原因 20 | error-image-not-set = {arg-image}が設定されていません 21 | error-image-open = '{$image_path}'を開くことができません 22 | error-image-metadata = '{$image_path}'からイメージのメタデータを取得できません 23 | error-disks-fetch = USBデバイスの一覧を取得できません 24 | error-no-disks-specified = デバイスが選択されていません 25 | error-fetching-mounts = マウントの一覧を取得できません 26 | error-opening-disks = デバイスを開けません 27 | error-exiting = 書き込まずに終了します 28 | error-reading-mounts = マウントを読み込むことに失敗 29 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "popsicle_cli" 3 | description = "USB Flasher" 4 | version = "1.3.3" 5 | authors = ["Jeremy Soller "] 6 | license = "MIT" 7 | readme = "README.md" 8 | edition = "2018" 9 | 10 | [[bin]] 11 | name = "popsicle" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | anyhow = "1.0.79" 16 | async-std = "1.12.0" 17 | atty = "0.2.14" 18 | better-panic = "0.3.0" 19 | cascade = "1.0.1" 20 | clap = "4.4.13" 21 | derive-new = "0.6.0" 22 | fomat-macros = "0.3.2" 23 | futures = "0.3.30" 24 | i18n-embed = { version = "0.14.1", features = ["fluent-system", "desktop-requester"] } 25 | i18n-embed-fl = "0.7.0" 26 | libc = "0.2.151" 27 | once_cell = "1.19.0" 28 | # pbr = "1.0.4" 29 | pbr = { git = "https://github.com/ids1024/pb", branch = "write" } 30 | popsicle = { path = ".." } 31 | rust-embed = { version = "8.2.0", features = ["debug-embed"] } 32 | -------------------------------------------------------------------------------- /i18n/ko/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = 선택한 기기에 '{$image_path}'를 플레시 하시겠습니까? 2 | 3 | yn = 예/아니오(y/N) 4 | y = 예 5 | 6 | # Arguments 7 | arg-image = 이미지 8 | arg-image-desc = 이미지 파일 선택 9 | 10 | arg-disks = 디스크 11 | arg-disks-desc = 디스크 기기 출력 12 | 13 | arg-all-desc = USB기기 모두 플래시 14 | arg-check-desc = 원본 이미지와 플레시된 이미지 비교 하기 15 | arg-unmount-desc = 마운트 기기 언마운트 하기 16 | arg-yes-desc = 승인 없이 진행 17 | 18 | # errors 19 | error-caused-by = 다음 사유로 에러 발생. 20 | #note that "(errormsg here)~으로 인해 에러 발생 may" be a better choice where the error 21 | error-image-not-set = {arg-image} 이미지 미 지정 22 | error-image-open = 위치한 이미지를 열 수 없습니다 '{$image_path}' 23 | error-image-metadata = 이미지 데이타를 열 수 없습니다 '{$image_path}' 24 | error-disks-fetch = USB 기기 목록을 구하지 못했습니다 25 | error-no-disks-specified = 기기를 지정하지 않았습니다 26 | error-fetching-mounts = 마운트 목록을 구하지 못했습니다 27 | error-opening-disks = 디스크를 열지 못했습니다 28 | error-exiting = 플래시 하지 않고 창 닫기 29 | error-reading-mounts = 마운트 기기를 읽는중 에러가 발생했습니다 30 | 31 | -------------------------------------------------------------------------------- /gtk/src/app/views/flashing.rs: -------------------------------------------------------------------------------- 1 | use super::View; 2 | use crate::fl; 3 | use gtk::{prelude::*, *}; 4 | 5 | pub struct FlashView { 6 | pub view: View, 7 | pub progress_list: Grid, 8 | } 9 | 10 | impl FlashView { 11 | pub fn new() -> FlashView { 12 | let progress_list = cascade! { 13 | Grid::new(); 14 | ..set_row_spacing(6); 15 | ..set_column_spacing(6); 16 | }; 17 | 18 | let progress_scroller = cascade! { 19 | ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); 20 | ..add(&progress_list); 21 | }; 22 | 23 | let view = View::new( 24 | "drive-removable-media-usb", 25 | &fl!("flash-view-title"), 26 | &fl!("flash-view-description"), 27 | |right_panel| right_panel.pack_start(&progress_scroller, true, true, 0), 28 | ); 29 | 30 | FlashView { view, progress_list } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /i18n/he/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = לצרוב את ‚{$image_path}’ לכוננים הבאים? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = דמות 8 | arg-image-desc = קובץ דמות קלט 9 | 10 | arg-disks = כוננים 11 | arg-disks-desc = כונני פלט 12 | 13 | arg-all-desc = צריבה לכל כונני ה־USB שזוהו 14 | arg-check-desc = לבדוק אם תמונה שנכתבת תואמת לדמות המקור 15 | arg-unmount-desc = ניתוק עיגון הכוננים המעוגנים 16 | arg-yes-desc = להמשיך ללא אישור 17 | 18 | # errors 19 | error-caused-by = נגרם על ידי 20 | error-image-not-set = {arg-image} לא הוגדר 21 | error-image-open = לא ניתן לפתוח את הדמות תחת ‚{$image_path}’ 22 | error-image-metadata = לא ניתן למשוך נתוני על של דמות דרך ‚{$image_path}’ 23 | error-disks-fetch = משיכת רשימת כונני ה־USB נכשלה 24 | error-no-disks-specified = לא צוינו כוננים 25 | error-fetching-mounts = משיכת רשימת העיגונים נכשלה 26 | error-opening-disks = פתיחת הכוננים נכשלה 27 | error-exiting = לצאת בלי לצרוב 28 | error-reading-mounts = שגיאה בקריאת העיגונים 29 | -------------------------------------------------------------------------------- /i18n/en/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Are you sure you want to flash '{$image_path}' to the following drives? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = IMAGE 8 | arg-image-desc = Input image file 9 | 10 | arg-disks = DISKS 11 | arg-disks-desc = Output disk devices 12 | 13 | arg-all-desc = Flash all detected USB drives 14 | arg-check-desc = Check if written image matches source image 15 | arg-unmount-desc = Unmount mounted devices 16 | arg-yes-desc = Continue without confirmation 17 | 18 | # errors 19 | error-caused-by = caused by 20 | error-image-not-set = {arg-image} not set 21 | error-image-open = unable to open image at '{$image_path}' 22 | error-image-metadata = unable to fetch image metadata at '{$image_path}' 23 | error-disks-fetch = failed to fetch list of USB disks 24 | error-no-disks-specified = no disks specified 25 | error-fetching-mounts = failed to fetch list of mounts 26 | error-opening-disks = failed to open disks 27 | error-exiting = exiting without flashing 28 | error-reading-mounts = error reading mounts 29 | -------------------------------------------------------------------------------- /i18n/da/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Er du sikker på du vile flashe '{$image_path}' til det følgende drev? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = BILLEDE 8 | arg-image-desc = Input image fil 9 | 10 | arg-disks = DISKS 11 | arg-disks-desc = Output dreve 12 | 13 | arg-all-desc = Flash alle genkendte USB drev 14 | arg-check-desc = Check hvis det skrevne billede matcher source image 15 | arg-unmount-desc = afmonter monterede drev 16 | arg-yes-desc = fortsæt uden konfirmation 17 | 18 | # errors 19 | error-caused-by = forårsaget af 20 | error-image-not-set = {arg-image} ikke sat 21 | error-image-open = kan ikke åbne image ved '{$image_path}' 22 | error-image-metadata = kan ikke fetche image metadata ved '{$image_path}' 23 | error-disks-fetch = kunne ikke fetche liste af USB drev 24 | error-no-disks-specified = ingen drev specificeret 25 | error-fetching-mounts = kunne ikke fetche liste af monteringspunkter 26 | error-opening-disks = kunne ikke åbne drev 27 | error-exiting = stoppede uden at flashing 28 | error-reading-mounts = kunne ikke læse monteringspunkter 29 | -------------------------------------------------------------------------------- /i18n/bn/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = তুমি কি আসলেই '{$image_path}' এই ড্রাইভে ইন্সটল করতে চাও? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = ইমেজ 8 | arg-image-desc = ইমেজ ফাইল যোগ করো 9 | 10 | arg-disks = ডিস্ক 11 | arg-disks-desc = কোন ডিস্ক যন্ত্রে ফ্ল্যাশ হবে? 12 | 13 | arg-all-desc = খোঁজ পাওয়া সব যন্ত্রে ফ্ল্যাশ করো 14 | arg-check-desc = লিখিত ইমেজ, উৎস ইমেজের সাথে মিলে কিনা যাচাই করো 15 | arg-unmount-desc = যুক্ত করা যন্ত্র বের(আনমাউন্ট) করো 16 | arg-yes-desc = নিশ্চিতকরণ ছাড়া আগাবে 17 | 18 | # errors 19 | error-caused-by = সমস্যার কারণ 20 | error-image-not-set = {arg-image} নির্দিষ্ট করা হয়নি 21 | error-image-open = '{$image_path}' এর ইমেজ খুলতে ব্যর্থ 22 | error-image-metadata = '{$image_path}' এ অবস্থিত ইমেজ সম্পর্কিত তথ্য আনতে ব্যর্থ 23 | error-disks-fetch = ইউএসবি ডিস্কের তালিকা পেতে ব্যর্থ 24 | error-no-disks-specified = কোনো ডিস্ক নির্দিষ্ট করা হয়নি 25 | error-fetching-mounts = মাউন্ট তালিকা পেতে ব্যর্থ 26 | error-opening-disks = ডিস্ক খুলতে ব্যর্থ 27 | error-exiting = ফ্ল্যাশ না করেই প্রস্থান করা হচ্ছে 28 | error-reading-mounts = মাউন্ট পড়তে ত্রুটি 29 | -------------------------------------------------------------------------------- /i18n/sv/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Är du säker på att du vill bränna '{$image_path}' till dessa diskar? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = OS-fil 8 | arg-image-desc = Inmatning av OS-fil 9 | 10 | arg-disks = ENHETER 11 | arg-disks-desc = Utmatningsenheter 12 | 13 | arg-all-desc = Bränn alla upphittade USB-enheter 14 | arg-check-desc = Kontrollera om den brända OS-filen matchar med ursprungsfilen 15 | arg-unmount-desc = Mata ut inmatade enheter 16 | arg-yes-desc = Fortsätt utan bekräftelse 17 | 18 | # errors 19 | error-caused-by = orsakad av 20 | error-image-not-set = ingen vald {arg-image} 21 | error-image-open = kunde inte öppna '{$image_path}' 22 | error-image-metadata = kunde inte hämta data från '{$image_path}' 23 | error-disks-fetch = inga giltiga enheter hittades 24 | error-no-disks-specified = ingen enhet vald 25 | error-fetching-mounts = kunde inte hitta data av monterade enheter 26 | error-opening-disks = kunde inte öppna enheter 27 | error-exiting = avslutade utan att bränna enhet 28 | error-reading-mounts = kunde inte läsa monterade enheter 29 | -------------------------------------------------------------------------------- /i18n/ru/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Вы уверены, что хотите записать '{$image_path}' на следующие диски? 2 | 3 | yn = y/n 4 | y = y 5 | 6 | # Arguments 7 | arg-image = ОБРАЗ 8 | arg-image-desc = Ввод файл образа 9 | 10 | arg-disks = ДИСК 11 | arg-disks-desc = Вывод дисковых устройств 12 | arg-all-desc = Записать на все обнаруженные USB-накопители 13 | 14 | arg-check-desc = Проверьте, соответствует ли записанный образ исходному 15 | arg-unmount-desc = Размонтировать устройства 16 | arg-yes-desc = Продолжить без подтверждения 17 | 18 | # errors 19 | error-caused-by = вызвано 20 | error-image-not-set = {arg-image} не задан 21 | error-image-open = не удалось открыть образ по '{$image_path}' 22 | error-image-metadata = не удалось получить метаданные образа по '{$image_path}' 23 | error-disks-fetch = не удалось получить список USB-дисков 24 | error-no-disks-specified = диски не указаны 25 | error-fetching-mounts = не удалось получить список монтирования 26 | error-opening-disks = не удалось открыть диски 27 | error-exiting = выход без записи 28 | error-reading-mounts = ошибка чтения монтирования 29 | -------------------------------------------------------------------------------- /i18n/cs/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Jste si jistí, že chcete flashnout '{$image_path}' do následujících disků? 2 | yn = y/N 3 | y = y 4 | 5 | # Arguments 6 | arg-image = OBRAZ 7 | arg-image-desc = Vložte obrazový soubor 8 | 9 | arg-disks = DISKY 10 | arg-disks-desc = Výstupní diskové zařízení 11 | 12 | arg-all-desc = Flashnout všechny zjištěné USB disky 13 | arg-check-desc = Zkontrolovat, zda zapsaný obraz sedí se vstupním obrazem 14 | arg-unmount-desc = Odpojit pripojená zařízení 15 | arg-yes-desc = Pokračovat bez potvrzení 16 | 17 | # errors 18 | error-caused-by = způsobeno 19 | error-image-not-set = {arg-image} není nastaven 20 | error-image-open = nelze otevřít obraz v '{$image_path}' 21 | error-image-metadata = nelze načíst metadata obrazu v '{$image_path}' 22 | error-disks-fetch = načtení seznamu USB disků selhalo 23 | error-no-disks-specified = nebyl specifikován disk 24 | error-fetching-mounts = načtení seznamu připojených disků selhalo 25 | error-opening-disks = otevření disků selhalo 26 | error-exiting = opouštění bez flashování 27 | error-reading-mounts = čtení připojených disků selhalo 28 | -------------------------------------------------------------------------------- /i18n/sl/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Ali ste prepričani, da želite '{$image_path}' utripati na naslednje pogone? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = SLIKA 8 | arg-image-desc = Vhodna slikovna datoteka 9 | 10 | arg-disks = DISKS 11 | arg-disks-desc = Izhodne diskovne naprave 12 | 13 | arg-all-desc = Utripaj vse zaznane USB pogone 14 | arg-check-desc = Preverite, ali se napisana slika ujema z izvorno sliko 15 | arg-unmount-desc = Odklopite nameščene naprave 16 | arg-yes-desc = Nadaljujte brez potrditve 17 | 18 | # errors 19 | error-caused-by = povzročil 20 | error-image-not-set = {arg-image} ni nastavljeno 21 | error-image-open = ne morem odpreti slike na '{$image_path}' 22 | error-image-metadata = ne morem pridobiti metapodatkov slike na '{$image_path}' 23 | error-disks-fetch = ni uspelo pridobiti seznama diskov USB 24 | error-no-disks-specified = noben disk ni določen 25 | error-fetching-mounts = ni uspelo pridobiti seznama vpenjanj 26 | error-opening-disks = ni uspelo odpreti diskov 27 | error-exiting = izhod brez utripanja 28 | error-reading-mounts = napaka pri branju nosilcev 29 | -------------------------------------------------------------------------------- /i18n/sq/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Jeni i sigurt se doni të shkruhet '{$image_path}' te pajisjet vijuese? 2 | 3 | yn = p/J 4 | y = p 5 | 6 | # Arguments 7 | arg-image = PAMJE 8 | arg-image-desc = Jepni kartelë pamjeje 9 | 10 | arg-disks = DISQE 11 | arg-disks-desc = Pajisje disk për shkrim 12 | 13 | arg-all-desc = Shkruaj krejt pajisjet USB të pikasura 14 | arg-check-desc = Kontrollo nëse pamja e shkruar përkon me pamjen burim 15 | arg-unmount-desc = Çmonto pajisje të montuara 16 | arg-yes-desc = Vazhdo pa ripohim 17 | 18 | # errors 19 | error-caused-by = shkaktuar nga 20 | error-image-not-set = {arg-image} s’është caktuar 21 | error-image-open = s’arrihet të hapet figura te '{$image_path}' 22 | error-image-metadata = s’arrihet të sillen tejtëdhëna pamjeje te '{$image_path}' 23 | error-disks-fetch = s’u arrit të sillet listë disqesh USB 24 | error-no-disks-specified = s’u përcaktuan disqe 25 | error-fetching-mounts = s’u arrit të sillet listë pikash montimi 26 | error-opening-disks = s’u arrit të hapen disqe 27 | error-exiting = po dilet pa shkruar gjë 28 | error-reading-mounts = gabim në lexim pikash montimi 29 | -------------------------------------------------------------------------------- /i18n/hi/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = क्या आप सुनिश्चित हैं कि आप निम्नलिखित ड्राइव पर '{$image_path}' फ्लैश करना चाहते हैं? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = इमेज 8 | arg-image-desc = इनपुट इमेज फ़ाइल 9 | 10 | arg-disks = डिस्क 11 | arg-disks-desc = आउटपुट डिस्क डिवाइस 12 | 13 | arg-all-desc = सभी पहचाने गए USB ड्राइव्स को फ्लैश करें 14 | arg-check-desc = जाँचें कि लिखी गई इमेज मैच करती है या नहीं 15 | arg-unmount-desc = माउंट किए गए डिवाइस को अनमाउंट करें 16 | arg-yes-desc = पुष्टि के बिना आगे बढ़ें 17 | 18 | # errors 19 | error-caused-by = कारण ये है कि 20 | error-image-not-set = {arg-image} सेट नहीं है। 21 | error-image-open = '{$image_path}' पर नहीं कोल पाई जा रही है। 22 | error-image-metadata = '{$image_path}' पर इमेज मेटाडेटा प्राप्त करने में असमर्थ 23 | error-disks-fetch = USB डिस्क की सूची प्राप्त करने में विफल। 24 | error-no-disks-specified = कोई डिस्क स्पष्ट नहीं की गई। 25 | error-fetching-mounts = माउंट की गई सूची प्राप्त करने में विफल। 26 | error-opening-disks = डिस्क खोलने में विफल। 27 | error-exiting = फ्लैश किए बिना बाहर निकल रहा है। 28 | error-reading-mounts = माउंट पठन में गड़बड़ी। 29 | -------------------------------------------------------------------------------- /i18n/sk/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Ste si istý že chcete flashnúť '{$image_path}' na nasledujúce disky? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = OBRAZ 8 | arg-image-desc = Vložte súbor obrazu 9 | 10 | arg-disks = DISKY 11 | arg-disks-desc = Výstupné diskové zariadenia 12 | 13 | arg-all-desc = Flashnúť všetky zistené USB disky 14 | arg-check-desc = Kontrola, či sa zapísaný obraz zhoduje so zdrojovým obrazom 15 | arg-unmount-desc = Odpojiť pripojené zariadenia 16 | arg-yes-desc = Pokračovať bez potvrdenia 17 | 18 | # errors 19 | error-caused-by = zapríčinené 20 | error-image-not-set = {arg-image} nenastavené 21 | error-image-open = nepodarilo sa otvoriť obraz v '{$image_path}' 22 | error-image-metadata = nepodarilo sa načítať metadáta obrazu v '{$image_path}' 23 | error-disks-fetch = načítanie zoznamu diskov USB zlyhalo 24 | error-no-disks-specified = nie sú zadané žiadne disky 25 | error-fetching-mounts = nepodarilo sa načítať zoznam pripojených diskov 26 | error-opening-disks = nepodarilo sa otvoriť disky 27 | error-exiting = odchod bez flashnutia 28 | error-reading-mounts = chybné čítanie pripojených diskov 29 | -------------------------------------------------------------------------------- /i18n/fi/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Oletko varma, että haluat kirjoittaa '{$image_path}' tähän asemaan? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = LEVYKUVA 8 | arg-image-desc = Valitse levykuva 9 | 10 | arg-disks = LEVYASEMAT 11 | arg-disks-desc = Käytettävissä olevat levyasemat 12 | 13 | arg-all-desc = Kirjoita kaikkiin havaittuihin USB-levyasemiin 14 | arg-check-desc = Tarkista, että levykuva täsmää alkuperäiseen levykuvaan 15 | arg-unmount-desc = Irroita liitetyt levyasemat 16 | arg-yes-desc = Jatka ilman vahvistusta 17 | 18 | # errors 19 | error-caused-by = Aiheutti 20 | error-image-not-set = {arg-image} ei asetettu 21 | error-image-open = Levykuvaa ei voitu avata '{$image_path}' 22 | error-image-metadata = Levykuvan metatietoja ei voitu hakea '{$image_path}' 23 | error-disks-fetch = USB-levyasemien haku epäonnistui 24 | error-no-disks-specified = Levyasemaa ei valittu 25 | error-fetching-mounts = Liitettyjen levyasemien listaa ei voitu hakea 26 | error-opening-disks = Levyasemien avaus epäonnistui 27 | error-exiting = Poistuu ilman kirjoittamista 28 | error-reading-mounts = Virhe luettaessa liitettyjä levyasemia 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 System76 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/src/localize.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader}, 3 | DefaultLocalizer, LanguageLoader, Localizer, 4 | }; 5 | use once_cell::sync::Lazy; 6 | use rust_embed::RustEmbed; 7 | 8 | #[derive(RustEmbed)] 9 | #[folder = "../i18n/"] 10 | struct Localizations; 11 | 12 | pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| { 13 | let loader: FluentLanguageLoader = fluent_language_loader!(); 14 | 15 | loader.load_fallback_language(&Localizations).expect("Error while loading fallback language"); 16 | 17 | loader 18 | }); 19 | 20 | #[macro_export] 21 | macro_rules! fl { 22 | ($message_id:literal) => {{ 23 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) 24 | }}; 25 | 26 | ($message_id:literal, $($args:expr),*) => {{ 27 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) 28 | }}; 29 | } 30 | 31 | // Get the `Localizer` to be used for localizing this library. 32 | pub fn localizer() -> Box { 33 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 34 | } 35 | -------------------------------------------------------------------------------- /gtk/src/localize.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::{ 2 | fluent::{fluent_language_loader, FluentLanguageLoader}, 3 | DefaultLocalizer, LanguageLoader, Localizer, 4 | }; 5 | use once_cell::sync::Lazy; 6 | use rust_embed::RustEmbed; 7 | 8 | #[derive(RustEmbed)] 9 | #[folder = "../i18n/"] 10 | struct Localizations; 11 | 12 | pub static LANGUAGE_LOADER: Lazy = Lazy::new(|| { 13 | let loader: FluentLanguageLoader = fluent_language_loader!(); 14 | 15 | loader.load_fallback_language(&Localizations).expect("Error while loading fallback language"); 16 | 17 | loader 18 | }); 19 | 20 | #[macro_export] 21 | macro_rules! fl { 22 | ($message_id:literal) => {{ 23 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) 24 | }}; 25 | 26 | ($message_id:literal, $($args:expr),*) => {{ 27 | i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) 28 | }}; 29 | } 30 | 31 | // Get the `Localizer` to be used for localizing this library. 32 | pub fn localizer() -> Box { 33 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 34 | } 35 | -------------------------------------------------------------------------------- /i18n/pt/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Gravar '{$image_path}' nos seguintes dispositivos? 2 | 3 | yn = s/N 4 | y = s 5 | 6 | # Arguments 7 | arg-image = IMAGEM 8 | arg-image-desc = Ficheiro de imagem de entrada 9 | 10 | arg-disks = DISPOSITIVOS 11 | arg-disks-desc = Dispositivos de saída 12 | 13 | arg-all-desc = Gravar todos os dispositivos USB detetados 14 | arg-check-desc = Verificar se a imagem gravada corresponde à imagem de origem 15 | arg-unmount-desc = Desmontar dispositivos montados 16 | arg-yes-desc = Prosseguir sem confirmação 17 | 18 | # errors 19 | error-caused-by = causado por 20 | error-image-not-set = {arg-image} não definida 21 | error-image-open = não foi possível abrir a imagem em '{$image_path}' 22 | error-image-metadata = não foi possível pesquisar metadados da imagem em '{$image_path}' 23 | error-disks-fetch = falha ao pesquisar dispositivos USB 24 | error-no-disks-specified = nenhum dipositivo especificado 25 | error-fetching-mounts = falha na pesquisa de pontos de montagem 26 | error-opening-disks = falha ao abrir dispositivos 27 | error-exiting = sair sem gravar 28 | error-reading-mounts = erro ao ler os pontos de montagem 29 | -------------------------------------------------------------------------------- /i18n/hu/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Ki szeretnéd írni a '{$image_path}' a következő eszközökre? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = LEMEZKÉP 8 | arg-image-desc = kiírandó lemezkép 9 | 10 | arg-disks = ESZKÖZŐK 11 | arg-disks-desc = Eszközök amelyekre ki lesz írva 12 | 13 | arg-all-desc = Írás az összes észlelt eszközre 14 | arg-check-desc = A kiírt kép és forrás kép összehasonlítása 15 | arg-unmount-desc = Leválasztása a felcsatolt eszközöknek 16 | arg-yes-desc = Folytatás megerősítés nélkül 17 | 18 | # errors 19 | error-caused-by = miatt 20 | error-image-not-set = {arg-image} nincs beállítva 21 | error-image-open = a lemezképet nem lehet megnyitni a '{$image_path}' elérési úton 22 | error-image-metadata = nem lehet elérni a lemezkép metaadatait a '{$image_path} elérési úton' 23 | error-disks-fetch = nem sikerült a csatlakoztatott eszközök listáját elérni 24 | error-no-disks-specified = nincs eszköz megadva 25 | error-fetching-mounts = nem sikerült a felcsatolt eszközök listáját elérni 26 | error-opening-disks = nem sikerült a lemezt elérni 27 | error-exiting = kilépés írás nélkül 28 | error-reading-mounts = hiba a felcsatolás közben 29 | -------------------------------------------------------------------------------- /i18n/it/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Sei sicuro di voler caricare '{$image_path}' nelle seguenti unità USB? 2 | 3 | yn = s/N 4 | y = s 5 | 6 | # Arguments 7 | arg-image = IMMAGINE 8 | arg-image-desc = Input file ISO 9 | 10 | arg-disks = Unità USB 11 | arg-disks-desc = Output 12 | 13 | arg-all-desc = Crea unità in tutte le unità USB 14 | arg-check-desc = Controlla se l'immagine scritta corrisponde all'immagine di origine 15 | arg-unmount-desc = Formatta le unità USB create 16 | arg-yes-desc = Continua senza conferma 17 | 18 | # errors 19 | error-caused-by = causato da 20 | error-image-not-set = {arg-image} non configurata 21 | error-image-open = impossibile aprire l'immagine su '{$image_path}' 22 | error-image-metadata = impossibile recuperare i metadati dell'immagine su '{$image_path}' 23 | error-disks-fetch = impossibile recuperare l'elenco delle unità USB 24 | error-no-disks-specified = nessuna unità specificata 25 | error-fetching-mounts = impossibile recuperare l'elenco delle unità USB create 26 | error-opening-disks = impossibile aprire le unità USB 27 | error-exiting = esci senza creare 28 | error-reading-mounts = errore di lettura delle unità USB create 29 | -------------------------------------------------------------------------------- /i18n/ca/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Segur que vols escriure '{$image_path}' als dispositius següents? 2 | 3 | yn = s/N 4 | y = s 5 | 6 | # Arguments 7 | arg-image = IMATGE 8 | arg-image-desc = Fitxer de imatge d'entrada 9 | 10 | arg-disks = DISCS 11 | arg-disks-desc = Dispositius de sortida 12 | 13 | arg-all-desc = Escriure tots els dispositius USB detectats 14 | arg-check-desc = Comprovar si la imatge escrita coincideix amb la imatge d'origen 15 | arg-unmount-desc = Desmuntar els dispositius muntats 16 | arg-yes-desc = Continuar sense confirmació 17 | 18 | # errors 19 | error-caused-by = causat per 20 | error-image-not-set = {arg-image} no definida 21 | error-image-open = no s'ha pogut obrir la imatge a '{$image_path}' 22 | error-image-metadata = no s'ha pogut trobar les metadades de la imatge a '{$image_path}' 23 | error-disks-fetch = error al buscar la llista de dispositius USB 24 | error-no-disks-specified = cap disc seleccionat 25 | error-fetching-mounts = error al buscar la llista de punts de muntatge 26 | error-opening-disks = error al obrir els dispositius 27 | error-exiting = sortint sense escriure 28 | error-reading-mounts = error al llegir els punts de muntatge 29 | -------------------------------------------------------------------------------- /i18n/pt-BR/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Você tem certeza de que quer gravar '{$image_path}' nos seguintes dispositivos? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = IMAGEM 8 | arg-image-desc = Arquivo de imagem de entrada 9 | 10 | arg-disks = DISPOSITIVOS 11 | arg-disks-desc = Dispositivos de saída 12 | 13 | arg-all-desc = Gravar todos os dispositivos USB detectados 14 | arg-check-desc = Verificar se a imagem gravada corresponde à imagem de origem 15 | arg-unmount-desc = Desmontar dispositivos montados 16 | arg-yes-desc = Prosseguir sem confirmação 17 | 18 | # errors 19 | error-caused-by = causado por 20 | error-image-not-set = {arg-image} não definida 21 | error-image-open = não foi possível abrir a imagem em '{$image_path}' 22 | error-image-metadata = não foi possível buscar metadados da imagem em '{$image_path}' 23 | error-disks-fetch = falha ao buscar lista de dispositivos USB 24 | error-no-disks-specified = nenhum dipositivo especificado 25 | error-fetching-mounts = falha ao buscar lista de pontos de montagem 26 | error-opening-disks = falha ao abrir dispositivos 27 | error-exiting = saindo sem gravar 28 | error-reading-mounts = erro lendo pontos de montagem 29 | -------------------------------------------------------------------------------- /i18n/zh-CN/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB 镜像刷入 2 | 3 | # Images View 4 | cannot-select-directories = 文件选择器无法选择路径 5 | check-label = 检查 6 | choose-image-button = 选择镜像 7 | generating-checksum = 正在创建校验值 8 | hash-label = Hash: 9 | image-view-description = 请选择你想刷入的 .iso 或 .img。你现在也可以插入 USB 闪存盘。 10 | image-view-title = 选择镜像 11 | no-image-selected = 没有选择镜像 12 | none = 无 13 | warning = 警告: 14 | 15 | # Devices View 16 | device-too-small = 设备太小 17 | devices-view-description = 刷入会抹除掉目标磁盘的所有数据。 18 | devices-view-title = 选择磁盘 19 | select-all = 全选 20 | 21 | # Flashing View 22 | flash-view-description = 请勿在刷入过程中拔出设备。 23 | flash-view-title = 正在刷入 24 | 25 | # Summary View 26 | flashing-completed = 刷入完成 27 | flashing-completed-with-errors = 刷入完成但出错 28 | flash-again = 重刷 29 | 30 | # Error View 31 | critical-error = 发生严重错误 32 | 33 | # Misc 34 | cancel = 取消 35 | close = 关闭 36 | done = 完成 37 | next = 下一步 38 | open = 打开 39 | task-finished = 完成 40 | 41 | # Events 42 | error = 错误:{$why} 43 | partial-flash = {$total} 中的 {$number} 个设备已成功刷入 44 | successful-flash = {$total} 个设备已成功刷入 45 | win-isos-not-supported = 目前不支持 Windows 的 ISO 46 | 47 | # Errors 48 | iso-open-failed = 无法打开 ISO 49 | no-value-found = 没有发现值 50 | -------------------------------------------------------------------------------- /i18n/nl/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Weet u zeker dat u '{$image_path}' naar de volgende schijven wilt flashen? 2 | 3 | yn = j/N 4 | y = j 5 | 6 | # Arguments 7 | arg-image = SCHIJFKOPIE 8 | arg-image-desc = Kies een schijfkopie 9 | 10 | arg-disks = SCHIJVEN 11 | arg-disks-desc = Beschikbare schijven selecteren 12 | 13 | arg-all-desc = Alle gedetecteerde usb-schijven flashen 14 | arg-check-desc = Controleer of de geschreven schijfkopie overeenkomt met de bron 15 | arg-unmount-desc = Ontkoppel aangekoppelde apparaten 16 | arg-yes-desc = Doorgaan zonder bevestiging 17 | 18 | # errors 19 | error-caused-by = veroorzaakt door 20 | error-image-not-set = {arg-image} niet ingesteld 21 | error-image-open = kan de schijfkopie op '{$image_path}' niet openen 22 | error-image-metadata = kan de metadata van de schijfkopie op '{$image_path}' niet ophalen 23 | error-disks-fetch = kon de lijst van usb-schijven niet ophalen 24 | error-no-disks-specified = geen schijven gespecificeerd 25 | error-fetching-mounts = kon de lijst van gekoppelde schijven niet ophalen 26 | error-opening-disks = kon schijven niet openen 27 | error-exiting = afsluiten zonder te flashen 28 | error-reading-mounts = kon gekoppelde schijven niet lezen 29 | -------------------------------------------------------------------------------- /gtk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "popsicle_gtk" 3 | description = "USB Flasher" 4 | version = "1.3.3" 5 | authors = [ "Michael Aaron Murphy " ] 6 | license = "MIT" 7 | readme = "README.md" 8 | edition = "2018" 9 | 10 | [[bin]] 11 | name = "popsicle-gtk" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | atomic = "0.6.0" 16 | anyhow = "1.0.79" 17 | bytemuck = "1.14.0" 18 | bytesize = "1.3.0" 19 | cascade = "1.0.1" 20 | crossbeam-channel = "0.5.10" 21 | dbus = "0.9.7" 22 | dbus-udisks2 = { git = "https://github.com/pop-os/dbus-udisks2" } 23 | digest = "0.10.7" 24 | futures = "0.3.30" 25 | gdk = "0.17.1" 26 | gio = "0.17.10" 27 | glib = "0.17.10" 28 | gtk = { version = "0.17.1" } 29 | hex-view = "0.1.3" 30 | iso9660 = { git = "https://github.com/ids1024/iso9660-rs" } 31 | libc = "0.2.151" 32 | md-5 = "0.10.6" 33 | pango = "0.17.10" 34 | popsicle = { path = ".." } 35 | pwd = "1.4.0" 36 | sha2 = "0.10.8" 37 | sha-1 = { version = "0.10.1", features = ["asm"] } 38 | i18n-embed = { version = "0.14.1", features = ["fluent-system", "desktop-requester"] } 39 | i18n-embed-fl = "0.7.0" 40 | rust-embed = { version = "8.2.0", features = ["debug-embed"] } 41 | once_cell = "1.19.0" 42 | blake2 = "0.10.6" 43 | -------------------------------------------------------------------------------- /i18n/bg/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Сигурни ли сте, че искате да запишете „{$image_path}“ на следните устройства? 2 | 3 | yn = y/N 4 | y = y 5 | 6 | # Arguments 7 | arg-image = ОБРАЗ 8 | arg-image-desc = Входен файл с образ 9 | 10 | arg-disks = УСТРОЙСТВА 11 | arg-disks-desc = Изходни дискови устройства 12 | 13 | arg-all-desc = Записване на всички открити USB устройства 14 | arg-check-desc = Check if written image matches source image 15 | arg-unmount-desc = Демонтиране на монтираните устройства 16 | arg-yes-desc = Продължаване без потвърждение 17 | 18 | # errors 19 | error-caused-by = причинено от 20 | error-image-not-set = {arg-image} не е зададено 21 | error-image-open = Образът в „{$image_path}“ не може да бъде отворен 22 | error-image-metadata = метаданните на образа в „{$image_path}“ не може да бъдат получени 23 | error-disks-fetch = списъкът с USB устройствата не може да бъде получен 24 | error-no-disks-specified = няма посочени устройства 25 | error-fetching-mounts = списъкът с монтираните устройства не може да бъде получен 26 | error-opening-disks = устройствата не може да бъдат отворени 27 | error-exiting = прекратяване без записване 28 | error-reading-mounts = грешка при четене на монтираните устройства 29 | -------------------------------------------------------------------------------- /i18n/de/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Sind sie sicher, dass sie '{$image_path}' auf folgende Datenträger schreiben möchten? 2 | 3 | yn = J/N 4 | y = j 5 | 6 | # Arguments 7 | arg-image = ABBILD 8 | arg-image-desc = Abbild auswählen 9 | 10 | arg-disks = DATENTRÄGER 11 | arg-disks-desc = Verfügbare Datenträger 12 | 13 | arg-all-desc = Alle USB-Datenträger flashen 14 | arg-check-desc = Überprüfen, ob das geschriebene Abbild mit dem Quellabbild übereinstimmt 15 | arg-unmount-desc = Gemountete Datenträger aushängen 16 | arg-yes-desc = Ohne Bestätigung fortfahren 17 | 18 | # errors 19 | error-caused-by = verursacht von 20 | error-image-not-set = {arg-image} nicht ausgewählt 21 | error-image-open = Kann Abbild nicht öffnen auf '{$image_path}' 22 | error-image-metadata = Kann Abbild-Metadateien nicht abrufen auf '{$image_path}' 23 | error-disks-fetch = Liste der USB-Datenträger konnte nicht abgerufen werden 24 | error-no-disks-specified = Keine Datenträger ausgewählt 25 | error-fetching-mounts = Liste der gemounteten Datenträger konnte nicht abgerufen werden 26 | error-opening-disks = Öffnen der Datenträgers fehlgeschlagen 27 | error-exiting = Beenden ohne Schreiben 28 | error-reading-mounts = Fehler beim Lesen der gemounteten Datenträger 29 | -------------------------------------------------------------------------------- /i18n/sr/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = Da li ste sigurni da želite da pomoću '{$image_path}' napravite sledeće uređaje instalacionim uređajima? 2 | 3 | yn = d/N 4 | y = d 5 | 6 | # Arguments 7 | arg-image = FAJL 8 | arg-image-desc = Ulazni instalacioni fajl 9 | 10 | arg-disks = UREĐAJI 11 | arg-disks-desc = Izlazni uređaji 12 | 13 | arg-all-desc = Napravi instalacione uređaje od svih detektovanih USB uređaja 14 | arg-check-desc = Proveri da li kreiran instalacioni uređaj odgovara izvornom instalacionom fajlu 15 | arg-unmount-desc = Isključi priključene uređaje 16 | arg-yes-desc = Nastavi bez potvrde 17 | 18 | # errors 19 | error-caused-by = uzrokovan 20 | error-image-not-set = {arg-image} nije postavljen 21 | error-image-open = nemoguće otvaranje fajla sa lokacije '{$image_path}' 22 | error-image-metadata = nemoguće prikupljanje metapodataka sa lokacije '{$image_path}' 23 | error-disks-fetch = nemoguće generisanje liste dostupnih USB uređaja 24 | error-no-disks-specified = uređaj nije odabran 25 | error-fetching-mounts = nemoguće prikupljanje liste priključnih tačaka 26 | error-opening-disks = nemoguće otvaranje uređaja 27 | error-exiting = izlazak bez kreiranja instalacionog uređaja 28 | error-reading-mounts = greška pri čitanju priključnih tačaka 29 | -------------------------------------------------------------------------------- /i18n/tr/popsicle_cli.ftl: -------------------------------------------------------------------------------- 1 | question = '{$image_path}' konumundaki disk görüntüsünü listelenen bellek aygıtlarına yazdırmak istediğinize emin misiniz? 2 | 3 | yn = e/H 4 | y = e 5 | 6 | # Arguments 7 | arg-image = DİSK GÖRÜNTÜSÜ 8 | arg-image-desc = (Girdi) disk görüntüsü dosyası 9 | 10 | arg-disks = DİSKLER 11 | arg-disks-desc = (Çıktı) bellek aygıtı 12 | 13 | arg-all-desc = Algılanan tüm USB bellek aygıtlarına yazdır 14 | arg-check-desc = Yazılan disk görüntüsünün kaynak disk görüntüsüyle aynı olup olmadığını kontrol et 15 | arg-unmount-desc = Bağlı bellek aygıtlarının bağlantısını kes 16 | arg-yes-desc = Onay almadan devam et 17 | 18 | # errors 19 | error-caused-by = sebep: 20 | error-image-not-set = {arg-image} seçilmedi. 21 | error-image-open = '{$image_path}' konumundaki disk görüntüsü açılamadı. 22 | error-image-metadata = '{$image_path}' konumundaki disk görüntüsünün üst verisine ulaşılamadı. 23 | error-disks-fetch = USB bellek aygıtlarının listesine ulaşılamadı. 24 | error-no-disks-specified = Bellek aygıtı seçilmedi. 25 | error-fetching-mounts = Bağlı cihazların listesine ulaşılamadı. 26 | error-opening-disks = Bellek aygıtı açılamadı. 27 | error-exiting = Yazdırılmadan sonlandırılıyor. 28 | error-reading-mounts = Bağlı bellek aygıtları okunurken bir hata oluştu. 29 | -------------------------------------------------------------------------------- /com.system76.Popsicle.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id" : "com.system76.Popsicle", 3 | "runtime" : "org.freedesktop.Platform", 4 | "runtime-version" : "22.08", 5 | "sdk" : "org.freedesktop.Sdk", 6 | "sdk-extensions" : [ 7 | "org.freedesktop.Sdk.Extension.rust-stable" 8 | ], 9 | "build-options" : { 10 | "append-path" : "/usr/lib/sdk/rust-stable/bin", 11 | "env" : { 12 | "CARGO_HOME" : "/run/build/popsicle/cargo" 13 | } 14 | }, 15 | "finish-args" : [ 16 | "--share=ipc", 17 | "--socket=fallback-x11", 18 | "--socket=wayland", 19 | "--system-talk-name=org.freedesktop.UDisks2" 20 | ], 21 | "command" : "popsicle-gtk", 22 | "cleanup" : [ 23 | "/share/icons/hicolor/512x512@2x" 24 | ], 25 | "modules" : [ 26 | { 27 | "name" : "popsicle", 28 | "buildsystem" : "simple", 29 | "sources" : [ 30 | { 31 | "type" : "dir", 32 | "path" : "." 33 | }, 34 | "generated-sources.json" 35 | ], 36 | "build-commands" : [ 37 | "make", 38 | "make install prefix=/app" 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /gtk/src/app/widgets/header.rs: -------------------------------------------------------------------------------- 1 | use crate::fl; 2 | use gtk::{prelude::*, *}; 3 | 4 | pub struct Header { 5 | pub container: HeaderBar, 6 | pub back: Button, 7 | pub next: Button, 8 | } 9 | 10 | impl Header { 11 | pub fn new() -> Header { 12 | let back = cascade! { 13 | Button::with_label(&fl!("cancel")); 14 | ..style_context().add_class("back"); 15 | }; 16 | 17 | let next = cascade! { 18 | Button::with_label(&fl!("next")); 19 | ..set_sensitive(false); 20 | ..style_context().add_class(&STYLE_CLASS_SUGGESTED_ACTION); 21 | }; 22 | 23 | // Returns the header and all of it's state 24 | Header { 25 | container: cascade! { 26 | HeaderBar::new(); 27 | ..set_title(Some(&fl!("app-title"))); 28 | ..pack_start(&back); 29 | ..pack_end(&next); 30 | }, 31 | back, 32 | next, 33 | } 34 | } 35 | 36 | pub fn connect_back(&self, signal: F) { 37 | self.back.connect_clicked(move |_| signal()); 38 | } 39 | 40 | pub fn connect_next(&self, signal: F) { 41 | self.next.connect_clicked(move |_| signal()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | release: 5 | types: [published] 6 | 7 | name: ci 8 | 9 | jobs: 10 | appimage: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | set-safe-directory: '*' 17 | - run: docker run --rm -v "$PWD:/github/workspace" -w "/github/workspace" rust:1.75.0-buster bash appimage.sh 18 | - uses: actions/upload-artifact@v4 19 | with: 20 | if-no-files-found: error 21 | name: popsicle-appimage-${{ github.sha }} 22 | path: Popsicle_USB_Flasher-*.AppImage* 23 | 24 | upload-to-release: 25 | if: github.event_name == 'release' 26 | runs-on: ubuntu-latest 27 | needs: appimage 28 | steps: 29 | - uses: actions/download-artifact@v4 30 | with: 31 | name: popsicle-appimage-${{ github.sha }} 32 | - run: printf 'APPIMAGE_FILENAME=%s\n' Popsicle_USB_Flasher-*.AppImage > $GITHUB_ENV 33 | - uses: actions/upload-release-asset@v1 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | upload_url: ${{ github.event.release.upload_url }} 38 | asset_path: ${{ env.APPIMAGE_FILENAME }} 39 | asset_name: ${{ env.APPIMAGE_FILENAME }} 40 | asset_content_type: application/vnd.appimage 41 | -------------------------------------------------------------------------------- /i18n/ja/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USBイメージ書き込み 2 | 3 | # Images View 4 | cannot-select-directories = ディレクトリを選択することができません 5 | check-label = チェック 6 | choose-image-button = イメージを選択 7 | generating-checksum = チェックサムを作成中 8 | hash-label = ハッシュ: 9 | image-view-description = 書き込みたい .iso または .img ファイルを選択してください。今USBデバイスを接続することもできます。 10 | image-view-title = イメージを選択してください 11 | no-image-selected = イメージが選択されていません 12 | none = なし 13 | warning = 注意: 14 | 15 | # Devices View 16 | device-too-small = デバイスの容量が小さすぎます 17 | devices-view-description = 書き込むと選択されたデバイスのデータがすべて削除されます。 18 | devices-view-title = デバイスの選択 19 | select-all = すべて選択 20 | 21 | # Flashing View 22 | flash-view-description = 書き込み中はデバイスを取り出さないでください。 23 | flash-view-title = 書き込み中 24 | 25 | # Summary View 26 | flashing-completed = 書き込み完了 27 | flashing-completed-with-errors = エラーとともに書き込み完了 28 | flash-again = もう一度書き込む 29 | 30 | # Error View 31 | critical-error = 致命的なエラーが発生しました 32 | 33 | # Misc 34 | cancel = キャンセル 35 | close = 閉じる 36 | done = 終了 37 | next = 次 38 | open = 開く 39 | task-finished = 完了 40 | 41 | # Events 42 | error = エラー: {$why} 43 | partial-flash = {$total}中{$number}個のデバイスに無事書き込みが完了しました 44 | successful-flash = {$total}個のデバイスに無事書き込みが完了しました 45 | win-isos-not-supported = WindowsのISOファイルは現在サポートされておりません 46 | 47 | # Errors 48 | iso-open-failed = ISOを開くことに失敗しました 49 | no-value-found = 値が見つかりませんでした 50 | -------------------------------------------------------------------------------- /gtk/src/app/widgets/dialogs.rs: -------------------------------------------------------------------------------- 1 | use crate::fl; 2 | use gtk::{prelude::*, *}; 3 | use std::path::PathBuf; 4 | 5 | /// A wrapped FileChooserNative that automatically destroys itself upon being dropped. 6 | pub struct OpenDialog(FileChooserNative); 7 | 8 | impl OpenDialog { 9 | pub fn new(path: Option) -> OpenDialog { 10 | #[allow(unused_mut)] 11 | OpenDialog(cascade! { 12 | let dialog = FileChooserNative::new( 13 | Some(&fl!("open")), 14 | Some(&Window::new(WindowType::Popup)), 15 | FileChooserAction::Open, 16 | Some(&fl!("open")), 17 | Some(&fl!("cancel")), 18 | ); 19 | ..set_filter(&cascade! { 20 | FileFilter::new(); 21 | ..add_pattern("*.[Ii][Ss][Oo]"); 22 | ..add_pattern("*.[Ii][Mm][Gg]"); 23 | }); 24 | if let Some(p) = path { 25 | dialog.set_current_folder(p); 26 | }; 27 | }) 28 | } 29 | 30 | pub fn run(&self) -> Option { 31 | if self.0.run() == ResponseType::Accept { 32 | self.0.filename() 33 | } else { 34 | None 35 | } 36 | } 37 | } 38 | 39 | impl Drop for OpenDialog { 40 | fn drop(&mut self) { 41 | self.0.destroy(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /i18n/ko/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB 플레시기 2 | 3 | # Images View 4 | cannot-select-directories = 다른 파일을 선택해 주세요 5 | check-label = 채크 6 | choose-image-button = 이미지를 선택하세요 7 | generating-checksum = 체크섬 생성중 8 | hash-label = Hash: 9 | image-view-description = 플래시 하고싶은 .iso 또는 .img 파일을 선택하세요. 지금USB 를 꽂으셔도 됩니다. 10 | image-view-title = 이미지를 선택하세요 11 | no-image-selected = 이미지 미선택 12 | none = 없음 13 | warning = Warning: 14 | 15 | # Devices View 16 | device-too-small = 기기 용량이 부족합니다 17 | devices-view-description = 플래시 하면 선택된 기기의 모든 데이터가 삭제 됩니다. 18 | devices-view-title = 기기를 선택 하세요 19 | select-all = 전부 선택 20 | 21 | # Flashing View 22 | flash-view-description = 플레시중 기기를 뽑지 마세요. 23 | flash-view-title = 플래시 진행 중 24 | 25 | # Summary View 26 | flashing-completed = 플레시 성공 27 | flashing-completed-with-errors = 오류와 함께 플레시 성공 28 | flash-again = 다시 플래시 하기 29 | 30 | # Error View 31 | critical-error = 에러 발생 32 | 33 | # Misc 34 | cancel = 취소 35 | close = 닫기 36 | done = 완료 37 | next = 다음 38 | open = 열기 39 | task-finished = 완성 40 | 41 | # Events 42 | error = 에러: {$why} 43 | partial-flash = {$total} 중 {$number}개의 기기 플래시 성공 44 | successful-flash = {$total}개의 기기 플래시 성공 45 | win-isos-not-supported = 윈도우즈 iso파일은 지원하지 않습니다 46 | 47 | # Errors 48 | iso-open-failed = iso를 열지 못했습니다 49 | no-value-found = 값이 미지정 입니다 50 | #with the above translation, there is a VScode visual bug. 값 will different from vscode and from an external editor. the vscode visuals are wrong 51 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | popsicle (1.3.3) jammy; urgency=medium 2 | 3 | * 1.3.3 release 4 | 5 | -- Michael Murphy Wed, 03 Jan 2024 16:24:46 +0100 6 | 7 | popsicle (1.3.2) jammy; urgency=medium 8 | 9 | * 1.3.2 release 10 | 11 | -- Michael Murphy Thu, 13 Jul 2023 17:34:00 +0200 12 | 13 | popsicle (1.3.0) groovy; urgency=medium 14 | 15 | * 1.3.0 release 16 | 17 | -- Ian Douglas Scott Thu, 05 Nov 2020 09:40:32 -0800 18 | 19 | popsicle (1.2.0) groovy; urgency=medium 20 | 21 | * 1.2.0 Release 22 | 23 | -- Ian Douglas Scott Tue, 27 Oct 2020 13:42:44 -0700 24 | 25 | popsicle (1.1.0) focal; urgency=medium 26 | 27 | * 1.1.0 release 28 | 29 | -- Ian Douglas Scott Mon, 03 Aug 2020 10:48:06 -0700 30 | 31 | popsicle (0.1.5) bionic; urgency=medium 32 | 33 | * 0.1.5 release 34 | 35 | -- Jeremy Soller Fri, 02 Mar 2018 12:48:11 -0700 36 | 37 | popsicle (0.1.4) bionic; urgency=medium 38 | 39 | * 0.1.4 release 40 | 41 | -- Jeremy Soller Tue, 30 Jan 2018 14:54:54 -0700 42 | 43 | popsicle (0.1.3) artful; urgency=medium 44 | 45 | * 0.1.3 release 46 | 47 | -- Jeremy Soller Tue, 07 Nov 2017 12:00:07 -0700 48 | 49 | popsicle (0.1.2) artful; urgency=medium 50 | 51 | * 0.1.2 release 52 | 53 | -- Jeremy Soller Mon, 16 Oct 2017 13:39:01 -0600 54 | -------------------------------------------------------------------------------- /i18n/he/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = צורב ל־USB 2 | 3 | # Images View 4 | cannot-select-directories = בורר הקבצים לא יכול לבחור תיקיות 5 | check-label = בדיקה 6 | choose-image-button = בחירת תמונה 7 | generating-checksum = נוצר סיכום ביקורת 8 | hash-label = גיבוב: 9 | image-view-description = נא לבחור את ה־.iso או את ה־.img שמיועד לצריבה. אפשר גם לחבר כעת את כונני ה־USB שלך. 10 | image-view-title = בחירת דמות 11 | no-image-selected = לא נבחרה דמות 12 | none = ללא 13 | warning = אזהרה: 14 | 15 | # Devices View 16 | device-too-small = הכונן קטן מדי 17 | devices-view-description = צריבה תמחק את כל הנתונים בכוננים הנבחרים. 18 | devices-view-title = בחירת כוננים 19 | select-all = בחירה בהכול 20 | 21 | # Flashing View 22 | flash-view-description = לא לנתק את ההתקנים תוך כדי צריבה. 23 | flash-view-title = התקנים נצרבים 24 | 25 | # Summary View 26 | flashing-completed = הצריבה הושלמה 27 | flashing-completed-with-errors = הצריבה הושלמה עם שגיאות 28 | flash-again = לצרוב שוב 29 | 30 | # Error View 31 | critical-error = אירעה שגיאה משמעותית 32 | 33 | # Misc 34 | cancel = ביטול 35 | close = סגירה 36 | done = בוצע 37 | next = הבא 38 | open = פתיחה 39 | task-finished = הושלמה 40 | 41 | # Events 42 | error = שגיאה: {$why} 43 | partial-flash = {$number} מתוך {$total} התקנים נצרבו בהצלחה 44 | successful-flash = {$total} התקנים נצרבו בהצלחה 45 | win-isos-not-supported = אין תמיכה בקובצי ISO של Windows 46 | 47 | # Errors 48 | iso-open-failed = פתיחת ה־ISO נכשלה 49 | no-value-found = לא נמצא ערך 50 | -------------------------------------------------------------------------------- /gtk/src/app/views/mod.rs: -------------------------------------------------------------------------------- 1 | mod devices; 2 | mod error; 3 | mod flashing; 4 | mod images; 5 | mod summary; 6 | mod view; 7 | 8 | pub use self::devices::DevicesView; 9 | pub use self::error::ErrorView; 10 | pub use self::flashing::FlashView; 11 | pub use self::images::ImageView; 12 | pub use self::summary::SummaryView; 13 | pub use self::view::View; 14 | 15 | use gtk::{prelude::*, *}; 16 | 17 | pub struct Content { 18 | pub container: Stack, 19 | pub image_view: ImageView, 20 | pub devices_view: DevicesView, 21 | pub error_view: ErrorView, 22 | pub flash_view: FlashView, 23 | pub summary_view: SummaryView, 24 | } 25 | 26 | impl Content { 27 | pub fn new() -> Content { 28 | let image_view = ImageView::new(); 29 | let devices_view = DevicesView::new(); 30 | let flash_view = FlashView::new(); 31 | let summary_view = SummaryView::new(); 32 | let error_view = ErrorView::new(); 33 | 34 | let container = cascade! { 35 | Stack::new(); 36 | ..add(&image_view.view.container); 37 | ..add(&devices_view.view.container); 38 | ..add(&flash_view.view.container); 39 | ..add(&summary_view.view.container); 40 | ..add(&error_view.view.container); 41 | ..set_visible_child(&image_view.view.container); 42 | ..set_border_width(12); 43 | }; 44 | 45 | Content { container, image_view, devices_view, flash_view, summary_view, error_view } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gtk/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unknown_lints)] 2 | 3 | #[macro_use] 4 | extern crate cascade; 5 | 6 | mod app; 7 | mod flash; 8 | mod gresource; 9 | mod hash; 10 | mod localize; 11 | mod misc; 12 | 13 | use crate::app::events::UiEvent; 14 | use crate::app::state::State; 15 | use crate::app::App; 16 | use i18n_embed::DesktopLanguageRequester; 17 | use std::env; 18 | use std::path::PathBuf; 19 | 20 | fn main() { 21 | let localizer = crate::localize::localizer(); 22 | let requested_languages = DesktopLanguageRequester::requested_languages(); 23 | 24 | if let Err(error) = localizer.select(&requested_languages) { 25 | eprintln!("Error while loading languages for library_fluent {}", error); 26 | } 27 | 28 | gtk::init().unwrap(); 29 | 30 | gresource::init().expect("failed to init popsicle gresource"); 31 | 32 | glib::set_program_name("Popsicle".into()); 33 | glib::set_application_name("Popsicle"); 34 | 35 | let app = App::new(State::new()); 36 | 37 | if let Some(iso_argument) = env::args().nth(1) { 38 | let path = PathBuf::from(iso_argument); 39 | if path.extension().map_or(false, |ext| { 40 | let lower_ext = ext.to_str().expect("Could not convert CStr to Str").to_lowercase(); 41 | lower_ext == "iso" || lower_ext == "img" 42 | }) && path.exists() 43 | { 44 | let _ = app.state.ui_event_tx.send(UiEvent::SetImageLabel(path)); 45 | } 46 | } 47 | 48 | app.connect_events().then_execute(); 49 | } 50 | -------------------------------------------------------------------------------- /i18n/da/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flasher 2 | 3 | # Images View 4 | cannot-select-directories = Fil vælger kan ikke vælge mapper 5 | check-label = Tjek 6 | choose-image-button = Vælg Image 7 | generating-checksum = Generere Checksum 8 | hash-label = Hash: 9 | image-view-description = vælg .iso eller .img'et du vil flashe. Du kan også proppe USB drev in nu. 10 | image-view-title = vælg et Image 11 | no-image-selected = Inget image valgt 12 | none = Ingen 13 | warning = Advarsel: 14 | 15 | # Devices View 16 | device-too-small = Drev for lille 17 | devices-view-description = Flashing vil slette alt data på de valgte drev. 18 | devices-view-title = Vælg Drev 19 | select-all = Vælg alle 20 | 21 | # Flashing View 22 | flash-view-description = Hiv ikke Drev ude før de er blevet flashed. 23 | flash-view-title = Flasher Drev 24 | 25 | # Summary View 26 | flashing-completed = Flashing Færdig 27 | flashing-completed-with-errors = Flashing Færdiggjorde uden Fejl 28 | flash-again = Flash igen 29 | 30 | # Error View 31 | critical-error = Kritisk Fejl 32 | 33 | # Misc 34 | cancel = Annuller 35 | close = Luk 36 | done = Færdig 37 | next = Næste 38 | open = Åben 39 | task-finished = Færdig 40 | 41 | # Events 42 | error = error: {$why} 43 | partial-flash = {$number} af {$total} drev flashede successfult 44 | successful-flash = {$total} drev flashede successfult 45 | win-isos-not-supported = Windows ISOer er ikke nuværende understøttet 46 | 47 | # Errors 48 | iso-open-failed = Fejlede i at åbne ISO 49 | no-value-found = ingen værdi fundet 50 | -------------------------------------------------------------------------------- /i18n/bn/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = ইউএসবি ফ্ল্যাশার 2 | 3 | # Images View 4 | cannot-select-directories = ফাইল নির্বাচক শাখা(ডাইরেক্টরি) নির্বাচন করতে পারেনি 5 | check-label = যাচাই 6 | choose-image-button = ইমেজ নির্বাচন 7 | generating-checksum = চেকসাম প্রস্তুত হচ্ছে 8 | hash-label = হ্যাশ : 9 | image-view-description = যে .iso বা .img ফাইল ফ্ল্যাশ করবে তা নির্বাচন করো। তোমার ইউএসবি ড্রাইভ বা পেনড্রাইভও এখন লাগাতে পারো। 10 | image-view-title = ইমেজ নির্বাচন 11 | no-image-selected = কোনো ইমেজ নির্বাচন করা হয়নি 12 | none = কোনোটাই না 13 | warning = সতর্কতা: 14 | 15 | # Devices View 16 | device-too-small = ডিভাইসের আকার খুব ছোট 17 | devices-view-description = ফ্ল্যাশ করলে নির্বাচিত ড্রাইভের সব তথ্য মুছে যাবে। 18 | devices-view-title = ড্রাইভ নির্বাচন 19 | select-all = সব নির্বাচন 20 | 21 | # Flashing View 22 | flash-view-description = ফ্ল্যাশরত অবস্থায় যন্ত্র খুলবে না। 23 | flash-view-title = ডিভাইসগুলো ফ্ল্যাশ করা হচ্ছে 24 | 25 | # Summary View 26 | flashing-completed = ফ্ল্যাশ করা শেষ 27 | flash-again = আবার ফ্ল্যাশ করো 28 | 29 | # Error View 30 | critical-error = সঙ্কটপূর্ণ ত্রুটি হয়েছে 31 | 32 | # Misc 33 | cancel = বাতিল 34 | close = বন্ধ 35 | next = তারপর 36 | open = খুলো 37 | task-finished = সম্পন্ন 38 | 39 | # Events 40 | error = ত্রুটি : {$why} 41 | partial-flash = {$total} এর {$number}টি সফলভাবে ফ্ল্যাশকৃত 42 | successful-flash = {$total}টি যন্ত্র সফলভাবে ফ্ল্যাশকৃত 43 | win-isos-not-supported = উইন্ডোজ এর ISO বর্তমানে সমর্থন করা হয় না 44 | 45 | # Errors 46 | iso-open-failed = ISO খুলতে ব্যর্থ 47 | no-value-found = কোনো মান পাওয়া যায়নি 48 | -------------------------------------------------------------------------------- /i18n/en/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flasher 2 | 3 | # Images View 4 | cannot-select-directories = File chooser can't select directories 5 | check-label = Check 6 | choose-image-button = Choose Image 7 | generating-checksum = Generating Checksum 8 | hash-label = Hash: 9 | image-view-description = Select the .iso or .img that you want to flash. You can also plug your USB drives in now. 10 | image-view-title = Choose an Image 11 | no-image-selected = No image selected 12 | none = None 13 | warning = Warning: 14 | 15 | # Devices View 16 | device-too-small = Device too small 17 | devices-view-description = Flashing will erase all data on the selected drives. 18 | devices-view-title = Select Drives 19 | select-all = Select all 20 | 21 | # Flashing View 22 | flash-view-description = Do not unplug devices while they are being flashed. 23 | flash-view-title = Flashing Devices 24 | 25 | # Summary View 26 | flashing-completed = Flashing Completed 27 | flashing-completed-with-errors = Flashing Completed with Errors 28 | flash-again = Flash Again 29 | 30 | # Error View 31 | critical-error = Critical Error Occurred 32 | 33 | # Misc 34 | cancel = Cancel 35 | close = Close 36 | done = Done 37 | next = Next 38 | open = Open 39 | task-finished = Complete 40 | 41 | # Events 42 | error = error: {$why} 43 | partial-flash = {$number} of {$total} devices successfully flashed 44 | successful-flash = {$total} devices successfully flashed 45 | win-isos-not-supported = Windows ISOs are not currently supported 46 | 47 | # Errors 48 | iso-open-failed = Failed to open ISO 49 | no-value-found = no value found 50 | -------------------------------------------------------------------------------- /gtk/src/misc.rs: -------------------------------------------------------------------------------- 1 | use dbus_udisks2::DiskDevice; 2 | use gtk::{self, prelude::*, SelectionData}; 3 | 4 | // Implements drag and drop support for a GTK widget. 5 | pub fn drag_and_drop(widget: &W, action: F) 6 | where 7 | W: WidgetExt + WidgetExtManual, 8 | F: 'static + Fn(&SelectionData), 9 | { 10 | // Configure the view as a possible drop destination. 11 | widget.drag_dest_set(gtk::DestDefaults::empty(), &[], gdk::DragAction::empty()); 12 | 13 | // Then actually handle drags that are inside the view. 14 | widget.connect_drag_motion(|_view, ctx, _x, _y, time| { 15 | ctx.drag_status(gdk::DragAction::COPY, time); 16 | true 17 | }); 18 | 19 | // Get the dropped data, if possible, when the active drag is valid. 20 | widget.connect_drag_drop(|view, ctx, _x, _y, time| { 21 | ctx.list_targets().last().map_or(false, |target| { 22 | view.drag_get_data(ctx, target, time); 23 | true 24 | }) 25 | }); 26 | 27 | // Then handle the dropped data, setting the image if the dropped data is valid. 28 | widget.connect_drag_data_received(move |_view, _ctx, _x, _y, data, _info, _time| action(data)); 29 | } 30 | 31 | pub fn device_label(device: &DiskDevice) -> String { 32 | if device.drive.vendor.is_empty() { 33 | format!("{} ({})", device.drive.model, device.parent.preferred_device.display()) 34 | } else { 35 | format!( 36 | "{} {} ({})", 37 | device.drive.vendor, 38 | device.drive.model, 39 | device.parent.preferred_device.display() 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /i18n/ru/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Программа записи на USB 2 | 3 | # Images View 4 | cannot-select-directories = Невозможно выбрать каталог 5 | check-label = Проверка 6 | choose-image-button = Выбрать образ 7 | generating-checksum = Подсчет контрольных сумм 8 | hash-label = Хэш: 9 | image-view-description = Выберите файл .iso или .img для записи. Также можно подключить устройства USB. 10 | image-view-title = Выбор образа 11 | no-image-selected = Нет выбранного образа 12 | none = Нет 13 | warning = Внимание: 14 | 15 | # Devices View 16 | device-too-small = Устройство слишком мало 17 | devices-view-description = Запись приведет к потере данных на выбранных дисках. 18 | devices-view-title = Выбрать диски 19 | select-all = Выбрать все 20 | 21 | # Flashing View 22 | flash-view-description = Не отключайте устройства до завершения записи. 23 | flash-view-title = Запись на устройства 24 | 25 | # Summary View 26 | flashing-completed = Запись завершена 27 | flashing-completed-with-errors = Запись завершена с ошибками 28 | flash-again = Записать снова 29 | 30 | # Error View 31 | critical-error = Произошла критическая ошибка 32 | 33 | # Misc 34 | cancel = Отмена 35 | close = Закрыть 36 | done = Готово 37 | next = Далее 38 | open = Открыть 39 | task-finished = Завершено 40 | 41 | # Events 42 | error = ошибка: {$why} 43 | partial-flash = успешно записано устройств: {$number} из {$total} 44 | successful-flash = успешно записано устройств: {$total} 45 | win-isos-not-supported = ISO Windows в настоящее время не поддерживаются 46 | 47 | # Errors 48 | iso-open-failed = Не удалось открыть ISO 49 | no-value-found = значение не найдено 50 | -------------------------------------------------------------------------------- /i18n/cs/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flashovač 2 | 3 | # Images View 4 | cannot-select-directories = Vyhledávač souborů nemůže vybírat složky 5 | check-label = Zkontrolovat 6 | choose-image-button = Vybrat obraz 7 | generating-checksum = Vytváření checksumu 8 | hash-label = Hash: 9 | image-view-description = Vyberte .iso nebo .img, který chcete flashnout. Můžete už také připojit vaše USB disky. 10 | image-view-title = Vyberte Obraz 11 | no-image-selected = Nebyl vybrán obraz 12 | none = Nic 13 | warning = Varování: 14 | 15 | # Devices View 16 | device-too-small = Zařízení je moc malé 17 | devices-view-description = Flashování smaže všechna data na vybraných discích. 18 | devices-view-title = Zvolte disky 19 | select-all = Zvolit vše 20 | 21 | # Flashing View 22 | flash-view-description = Neodpojujte zařízení během flashování. 23 | flash-view-title = Flashování Zařízení 24 | 25 | # Summary View 26 | flashing-completed = Flashování Dokončeno 27 | flashing-completed-with-errors = Flashování Dokončeno s Chybama 28 | flash-again = Flashnout Znova 29 | 30 | # Error View 31 | critical-error = Nastalo Kritické Selhání 32 | 33 | # Misc 34 | cancel = Zrušit 35 | close = Zavřít 36 | done = Hotovo 37 | next = Další 38 | open = Otevřít 39 | task-finished = Dokončeno 40 | 41 | # Events 42 | error = error: {$why} 43 | partial-flash = {$number} z {$total} celkových zařízení úspěšně flashnuto 44 | successful-flash = {$total} zařízení úspěšně flashnuto 45 | win-isos-not-supported = Windows obrazy zatím nejsou podporovány 46 | 47 | # Errors 48 | iso-open-failed = otevření ISO souboru selhalo 49 | no-value-found = nebyla nalezena hodnota 50 | -------------------------------------------------------------------------------- /i18n/sl/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Utripač 2 | 3 | # Images View 4 | cannot-select-directories = Izbirnik datotek ne more izbrati imenikov 5 | check-label = Prilepi 6 | choose-image-button = Izberi sliko 7 | generating-checksum = Ustvarjanje kontrolne vsote 8 | hash-label = Hash: 9 | image-view-description = Izberite .iso ali .img, ki ga želite utripati. Zdaj lahko tudi priključite pogone USB. 10 | image-view-title = Izberite sliko 11 | no-image-selected = Slika ni izbrana 12 | none = Nič 13 | warning = Opozorilo: 14 | 15 | # Devices View 16 | device-too-small = Naprava je premajhna 17 | devices-view-description = Utripanje bo izbrisalo vse podatke na izbranih pogonih. 18 | devices-view-title = Izbrani pogoni 19 | select-all = Izberi vse 20 | 21 | # Flashing View 22 | flash-view-description = Naprav ne izključite iz vtičnice, ko jih utripate. 23 | flash-view-title = Utripajoče naprave 24 | 25 | # Summary View 26 | flashing-completed = Utripanje končano 27 | flashing-completed-with-errors = Utripanje končano z napakami 28 | flash-again = Utripaj ponovno 29 | 30 | # Error View 31 | critical-error = Prišlo je do kritične napake 32 | 33 | # Misc 34 | cancel = Prekliči 35 | close = Zapri 36 | done = Končano 37 | next = Naprej 38 | open = Odpri 39 | task-finished = Dokončano 40 | 41 | # Events 42 | error = napaka: {$why} 43 | partial-flash = Uspešno je utripalo {$number} od {$total} naprav 44 | successful-flash = {$total} naprav je uspešno utripalo 45 | win-isos-not-supported = Windows ISO trenutno niso podprte 46 | 47 | # Errors 48 | iso-open-failed = ISO ni uspelo odpreti 49 | no-value-found = nobena vrednost ni najdena 50 | -------------------------------------------------------------------------------- /i18n/hu/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB lemezkép író 2 | 3 | # Images View 4 | cannot-select-directories = A fájl választó nem tud mappákat kiválasztani 5 | check-label = Ellenőrzés 6 | choose-image-button = Lemezkép kiválasztása 7 | generating-checksum = Checksum készítése 8 | hash-label = Hash: 9 | image-view-description = Válaszd ki a .iso vagy .img fájlt amit kiszeretnél íratni. Most tudod a pendrive-ot is behelyezni. 10 | image-view-title = Válasz ki egy lemezképet 11 | no-image-selected = Nincs lemezkép kiválasztva 12 | none = Semelyik 13 | warning = Figyelmeztetés: 14 | 15 | # Devices View 16 | device-too-small = Az eszközön kevés a tárhely 17 | devices-view-description = Az írás minden fájlt le fog törölni az eszközről. 18 | devices-view-title = Válaszd ki az eszközöket 19 | select-all = Összes kiválasztása 20 | 21 | # Flashing View 22 | flash-view-description = Ne távolítsd el az eszközöket amíg az írás folyamatban van. 23 | flash-view-title = Írás az eszközökre 24 | 25 | # Summary View 26 | flashing-completed = Az írás kész 27 | flash-again = Írás újra 28 | 29 | # Error View 30 | critical-error = Végzetes hiba történt 31 | 32 | # Misc 33 | cancel = Mégse 34 | close = Bezárás 35 | done = Kész 36 | next = Következő 37 | open = Megnyitás 38 | task-finished = Befejezve 39 | 40 | # Events 41 | error = hiba: {$why} 42 | partial-flash = {$number}-ből {$total} eszközöre sikerült az írás 43 | successful-flash = {$total} eszköz sikeresen kiírva 44 | win-isos-not-supported = Windows lemezképek jelenleg nincsenek támogatva 45 | 46 | # Errors 47 | iso-open-failed = Nem sikerült a lemezkép kiírása 48 | no-value-found = nincs érték 49 | -------------------------------------------------------------------------------- /i18n/pl/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Flasher USB 2 | 3 | # Images View 4 | cannot-select-directories = Wybieranie pliku nie może zaznaczyć katalogów 5 | check-label = Sprawdź 6 | choose-image-button = Wybierz Obraz 7 | generating-checksum = Generowanie Sumy Kontrolnej 8 | hash-label = Algorytm Skrótu: 9 | image-view-description = Zaznacz .iso lub .img które chcesz flashować. Teraz możesz również podłączyć swój dysk USB. 10 | image-view-title = Wybierz Obraz 11 | no-image-selected = Nie wybrano obrazu 12 | none = Brak 13 | warning = Ostrzeżenie: 14 | 15 | # Devices View 16 | device-too-small = Urządzenie zbyt małe 17 | devices-view-description = Flashing wymaże wszystkie dane na zaznaczonych dyskach 18 | devices-view-title = Zaznacz Dyski 19 | select-all = Zaznacz wszystko 20 | 21 | # Flashing View 22 | flashing-view-description = Nie odłączaj urządzeń kiedy trwa ich flashowanie 23 | flashing-view-title = Flashowane Urządzenia 24 | 25 | # Summary View 26 | flash-again = Ponów Flashowanie 27 | flashing-completed = Flashowanie Zakończone 28 | 29 | # Error View 30 | critical-error = Wystąpił Krytyczny Błąd 31 | 32 | # Misc 33 | cancel = Anuluj 34 | close = Zamknij 35 | done = Zakończ 36 | next = Następny 37 | open = Otwórz 38 | task-finished = Zakończono 39 | 40 | # Events 41 | error = błąd: {$why} 42 | partial-flash = {$number} z {$total} urządzeń z pomyślnie ukończonym flashowaniem 43 | successful-flash = Flashowanie {$total} urządzeń zakończone pomyślnie 44 | win-isos-not-supported = Windows ISOs nie są aktualnie wspierane 45 | 46 | # Errors 47 | iso-open-failed = Nie udało się otworzyć ISO 48 | no-value-found = nie wykryto wartości 49 | -------------------------------------------------------------------------------- /i18n/sv/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Brännare 2 | 3 | # Images View 4 | cannot-select-directories = Filväljaren kan inte välja en mappar 5 | check-label = Kontrollera 6 | choose-image-button = Välj fil 7 | generating-checksum = Genererar kontrollsumma 8 | hash-label = Kontrollfil: 9 | image-view-description = Välj .iso- eller .img-fil som du vill bränna till din USB-enhet. Du kan också välja att mata in din enhet redan nu, 10 | image-view-title = Välj fil 11 | no-image-selected = Ingen fil vald 12 | none = Ingen 13 | warning = Varning: 14 | 15 | # Devices View 16 | device-too-small = Enheten har inte tillräckligt med utrymme. 17 | devices-view-description = Genom att bränna denna enhet kommer all data att tas bort. 18 | devices-view-title = Välj enheter 19 | select-all = Markera alla 20 | 21 | # Flashing View 22 | flash-view-description = Mata ej ut enheter medans processen pågår. 23 | flash-view-title = Bränner enheter 24 | 25 | # Summary View 26 | flashing-completed = Bränning av enhet lyckades 27 | flashing-completed-with-errors = Fel uppstod under bränning av enhet. 28 | flash-again = Bränn igen 29 | 30 | # Error View 31 | critical-error = Kritiskt fel uppstod 32 | 33 | # Misc 34 | cancel = Avbryt 35 | close = Stäng 36 | done = Klar 37 | next = Nästa 38 | open = Öppna 39 | task-finished = Avsluta 40 | 41 | # Events 42 | error = fel: {$why} 43 | partial-flash = {$number} av {$total} enheter lyckades 44 | successful-flash = {$total} enheter lyckades 45 | win-isos-not-supported = Windows ISO-filer stöds för närvarande inte 46 | 47 | # Errors 48 | iso-open-failed = Kunde inte öppna ISO 49 | no-value-found = ingen data hittades 50 | -------------------------------------------------------------------------------- /i18n/de/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flasher 2 | 3 | # Images View 4 | cannot-select-directories = Verzeichnis konnte nicht ausgewählt werden 5 | check-label = Prüfen 6 | choose-image-button = Abbild auswählen 7 | generating-checksum = Generiere Checksum 8 | hash-label = Hash: 9 | image-view-description = Wählen Sie bitte die .iso oder die .img, die Sie flashen möchten. Sie können jetzt auch den USB-Datenträger einstecken. 10 | image-view-title = Abbild auswählen 11 | no-image-selected = Kein Abbild ausgewählt 12 | none = Keine 13 | warning = Warnung: 14 | 15 | # Devices View 16 | device-too-small = Datenträger zu klein 17 | devices-view-description = Beim Flashen werden alle Daten am Datenträger gelöscht 18 | devices-view-title = Datenträger auswählen 19 | select-all = Alle auswählen 20 | 21 | # Flashing View 22 | flash-view-description = Datenträger während des Flashens bitte nicht ausstecken 23 | flash-view-title = Geräte flashen... 24 | 25 | # Summary View 26 | flashing-completed = Flashing abgeschlossen 27 | flash-again = Nochmal flashen 28 | 29 | # Error View 30 | critical-error = Ein kritischer Fehler ist aufgetreten 31 | 32 | # Misc 33 | cancel = Abbrechen 34 | close = Schließen 35 | done = Fertig 36 | next = Nächstes 37 | open = Öffnen 38 | task-finished = Vollständig 39 | 40 | # Events 41 | error = Fehler: {$why} 42 | partial-flash = {$number} von {$total} Datenträger erfolgreich geflasht 43 | successful-flash = {$total} Datenträger erfolgreich geflasht 44 | win-isos-not-supported = Windows ISOs werden nicht unterstützt 45 | 46 | # Errors 47 | iso-open-failed = Fehler beim Öffnen der ISO 48 | no-value-found = Kein Wert gefunden -------------------------------------------------------------------------------- /i18n/sq/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flasher 2 | 3 | # Images View 4 | cannot-select-directories = Zgjedhësi i kartelave s’mund të përzgjedhë drejtori 5 | check-label = Check 6 | choose-image-button = Zgjidhni Pamje 7 | generating-checksum = Po prodhohet Checksum 8 | hash-label = Hash: 9 | image-view-description = Përzgjidhni .iso ose .img që doni të shkruhet. Mundeni edhe të futni tani disqet tuaj USB. 10 | image-view-title = Zgjidhni një Figurë 11 | no-image-selected = S’u përzgjodh figurë 12 | none = Asnjë 13 | warning = Kujdes: 14 | 15 | # Devices View 16 | device-too-small = Pajisje shumë e voglë 17 | devices-view-description = Shkrimi do të fshijë krejt të dhënat në disqet e përzgjedhur. 18 | devices-view-title = Përzgjidhni Disqe 19 | select-all = Përzgjidhi krejt 20 | 21 | # Flashing View 22 | flash-view-description = Mos i hiqni disqet, ndërkohë që në ta shkruhet. 23 | flash-view-title = Shkrim Pajisjesh 24 | 25 | # Summary View 26 | flashing-completed = Shkrimi u Plotësua 27 | flashing-completed-with-errors = Shkrimi u Plotësua me Gabime 28 | flash-again = Rishkruaje 29 | 30 | # Error View 31 | critical-error = Ndodhi një Gabim Kritik 32 | 33 | # Misc 34 | cancel = Anuloje 35 | close = Mbylle 36 | done = U bë 37 | next = Pasuesi 38 | open = Hape 39 | task-finished = I plotësuar 40 | 41 | # Events 42 | error = gabim: {$why} 43 | partial-flash = {$number} nga {$total} pajisje gjithsej janë shkruar me sukses 44 | successful-flash = {$total} pajisje gjithsej shkruar me sukses 45 | win-isos-not-supported = Aktualisht s’mbulohen pamje ISO Windows 46 | 47 | # Errors 48 | iso-open-failed = S’u arrit të hapet ISO 49 | no-value-found = s’u gjet vlerë 50 | -------------------------------------------------------------------------------- /i18n/fi/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flasher 2 | 3 | # Images View 4 | cannot-select-directories = Tiedostonvalitsin ei voi valita kansioita 5 | check-label = Tarkista 6 | choose-image-button = Valitse levykuva 7 | generating-checksum = Luodaan tarkistussummaa 8 | hash-label = Hash: 9 | image-view-description = Valitse .iso tai .img kirjoitettavaksi. Voit myös liittää USB-levyaseman tietokoneeseen nyt. 10 | image-view-title = Valitse levykuva 11 | no-image-selected = Levykuvaa ei valittu 12 | none = Ei mitään 13 | warning = Varoitus: 14 | 15 | # Devices View 16 | device-too-small = Laite liian pieni 17 | devices-view-description = Kirjoittaminen poistaa kaikki tiedot valituista levyasemista. 18 | devices-view-title = Valitse levyasema 19 | select-all = Valitse kaikki 20 | 21 | # Flashing View 22 | flash-view-description = Älä poista levyasemaa kirjoituksen aikana. 23 | flash-view-title = Kirjoittaa laitteita 24 | 25 | # Summary View 26 | flashing-completed = Kirjoittaminen valmis 27 | flashing-completed-with-errors = Kirjoitus onnistui ilman virheitä 28 | flash-again = Kirjoita uudelleen 29 | 30 | # Error View 31 | critical-error = Kriittisiä virheitä ilmeni 32 | 33 | # Misc 34 | cancel = Peru 35 | close = Sulje 36 | done = Valmis 37 | next = Seuraava 38 | open = Avaa 39 | task-finished = Valmis 40 | 41 | # Events 42 | error = virhe: {$why} 43 | partial-flash = {$number} / {$total} laitteesta kirjoitettu onnistuneesti 44 | successful-flash = {$total} laitetta kirjoitettu onnistuneesti 45 | win-isos-not-supported = Windows ISO ei ole tällä hetkellä tuettu 46 | 47 | # Errors 48 | iso-open-failed = ISO avaus epäonnistui 49 | no-value-found = Arvoa ei löytynyt 50 | -------------------------------------------------------------------------------- /i18n/sk/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Flashovač USB 2 | 3 | # Images View 4 | cannot-select-directories = Prieskumník súborov nemôže vybrať adresáre 5 | check-label = Skontrolovať 6 | choose-image-button = Vybrať obraz 7 | generating-checksum = Generujem Checksum 8 | hash-label = Hash: 9 | image-view-description = Vyberte .iso alebo .img, ktorý chcete naformátovať. Teraz môžete tiež pripojiť svoje USB disky. 10 | image-view-title = Zvoľte obraz 11 | no-image-selected = Nebol zvolený obraz 12 | none = Nič 13 | warning = Varovanie: 14 | 15 | # Devices View 16 | device-too-small = Zariadenie je príliš malé 17 | devices-view-description = Flashovanie vymaže všetok obsah na vybratých diskoch. 18 | devices-view-title = Vyberte zariadenia 19 | select-all = Vybrať všetko 20 | 21 | # Flashing View 22 | flash-view-description = Do not unplug devices while they are being flashed. 23 | flash-view-title = Flashovanie zariadení 24 | 25 | # Summary View 26 | flashing-completed = Flashovanie dokončené 27 | flashing-completed-with-errors = Flashovanie dokončené s chybami 28 | flash-again = Flashni znova 29 | 30 | # Error View 31 | critical-error = Vyskytla sa kritická chyba 32 | 33 | # Misc 34 | cancel = Zrušiť 35 | close = Zavrieť 36 | done = Hotovo 37 | next = Ďalej 38 | open = Otvoriť 39 | task-finished = Dokončené 40 | 41 | # Events 42 | error = error: {$why} 43 | partial-flash = {$number} z {$total} celkových zariadení úspešne flashnuté 44 | successful-flash = {$total} zariadení úspešne flashnuté 45 | win-isos-not-supported = Windows ISO súbory nie sú momentálne podporované 46 | 47 | # Errors 48 | iso-open-failed = Nepodarilo sa otvoriť ISO súbor 49 | no-value-found = žiadna hodnota nebola nájdená 50 | -------------------------------------------------------------------------------- /i18n/fr/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Flasheur USB 2 | 3 | # Images View 4 | cannot-select-directories = Le sélecteur de fichiers ne peut pas sélectionner de répertoire. 5 | check-label = Vérification 6 | choose-image-button = Choisir une image 7 | generating-checksum = Génération de la somme de contrôle... 8 | hash-label = Hash : 9 | image-view-description = Sélectionnez le fichier .iso ou .img que vous souhaitez flasher. Insérez aussi vos supports USB. 10 | image-view-title = Choisir une image 11 | no-image-selected = Aucune image sélectionnée 12 | none = Aucune 13 | warning = Attention : 14 | 15 | # Devices View 16 | device-too-small = Périphérique trop petit 17 | devices-view-description = Toutes les données des lecteurs sélectionnés seront effacées. 18 | devices-view-title = Sélectionner les lecteurs 19 | select-all = Tout sélectionner 20 | 21 | # Flashing View 22 | flash-view-description = Ne débranchez pas les périphériques pendant qu'ils sont en train d'être flashés. 23 | flash-view-title = Flash des périphériques 24 | 25 | # Summary View 26 | flashing-completed = Flash terminé 27 | flash-again = Flasher à nouveau 28 | 29 | # Error View 30 | critical-error = Une erreur critique est survenue. 31 | 32 | # Misc 33 | cancel = Annuler 34 | close = Fermer 35 | next = Suivant 36 | open = Ouvrir 37 | task-finished = Terminé 38 | 39 | # Events 40 | error = erreur : {$why} 41 | partial-flash = {$number} sur {$total} périphérique(s) flashé(s) correctement 42 | successful-flash = {$total} périphérique(s) flashé(s) correctement 43 | win-isos-not-supported = Les ISOs Windows ne sont pas supportées pour le moment 44 | 45 | # Errors 46 | iso-open-failed = Impossible d'ouvrir l'ISO 47 | no-value-found = Aucune valeur trouvée 48 | -------------------------------------------------------------------------------- /i18n/bg/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Flasher 2 | 3 | # Images View 4 | cannot-select-directories = Менюто за избор на файлове не може да избира папки 5 | check-label = Проверяване 6 | choose-image-button = Избиране на образ 7 | generating-checksum = Генериране на контролна сума 8 | hash-label = Хеш: 9 | image-view-description = Изберете .iso или .img файла, който искате да запишете. Сега може да включите и USB устройствата си. 10 | image-view-title = Избор на образ 11 | no-image-selected = Няма избран образ 12 | none = Без 13 | warning = Внимание: 14 | 15 | # Devices View 16 | device-too-small = Устройството няма достатъчно пространство 17 | devices-view-description = Записването ще изтрие всички данни от избраните дискове. 18 | devices-view-title = Избор на устройства 19 | select-all = Избиране на всички 20 | 21 | # Flashing View 22 | flash-view-description = Не премахвайте устройствата, докато се записват. 23 | flash-view-title = Устройства за записване 24 | 25 | # Summary View 26 | flashing-completed = Записването завърши 27 | flashing-completed-with-errors = Записването завърши с грешки 28 | flash-again = Запиване отново 29 | 30 | # Error View 31 | critical-error = Възникна критична грешка 32 | 33 | # Misc 34 | cancel = Прекратяване 35 | close = Затваряне 36 | done = Готово 37 | next = Напред 38 | open = Отваряне 39 | task-finished = Завършено 40 | 41 | # Events 42 | error = грешка: {$why} 43 | partial-flash = {$number} от {$total} устройства са успешно записани 44 | successful-flash = Успешно записани устройства: {$total} 45 | win-isos-not-supported = В момента не се поддържат ISO образи за Windows 46 | 47 | # Errors 48 | iso-open-failed = ISO образът не може да бъде отворен 49 | no-value-found = няма намерена стойност 50 | -------------------------------------------------------------------------------- /i18n/ca/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Creador de discs d'arrencada USB 2 | 3 | # Images View 4 | cannot-select-directories = El selector de fitxers no pot escollir carpetes 5 | check-label = Comprovar 6 | choose-image-button = Seleccionar imatge 7 | generating-checksum = Generant la suma de comprovació 8 | hash-label = Resum: 9 | image-view-description = Seleccioni el fitxer .iso o .img que vol escriure. També pot connectar dispositius USB ara. 10 | image-view-title = Seleccionar una imatge 11 | no-image-selected = Cap imatge seleccionada 12 | none = Cap 13 | warning = Atenció: 14 | 15 | # Devices View 16 | device-too-small = El dispositiu és massa petit 17 | devices-view-description = L'aplicació esborrarà totes les dades dels dispositius USB seleccionats. 18 | devices-view-title = Seleccionar dispositius 19 | select-all = Seleccionar tot 20 | 21 | # Flashing View 22 | flash-view-description = No desconnecti cap dispositiu durant el procés d'escriptura. 23 | flash-view-title = Escrivint la image als dispositius 24 | 25 | # Summary View 26 | flashing-completed = L'escriptura ha finalitzat 27 | flash-again = Tornar a escriure 28 | 29 | # Error View 30 | critical-error = S'ha produït un error greu 31 | 32 | # Misc 33 | cancel = Cancel·lar 34 | close = Tancar 35 | next = Següent 36 | open = Obrir 37 | task-finished = Completat 38 | 39 | # Events 40 | error = error: {$why} 41 | partial-flash = La imatge s'ha escrit correctament en {$number} de {$total} dispositius 42 | successful-flash = La imatge s'ha escrit correctament en {$total} dispositiu(s) 43 | win-isos-not-supported = Actualment no s'admeten fitxers ISO de Windows 44 | 45 | # Errors 46 | iso-open-failed = No s'ha pogut obrir el fitxer ISO 47 | no-value-found = no s'ha trobat cap valor 48 | -------------------------------------------------------------------------------- /i18n/pt-BR/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Gravador USB 2 | 3 | # Images View 4 | cannot-select-directories = O seletor de arquivos não pode selecionar diretórios 5 | check-label = Verificar 6 | choose-image-button = Escolher imagem 7 | generating-checksum = Gerando soma de verificação 8 | hash-label = Hash: 9 | image-view-description = Selecione a .iso ou .img que você quer gravar. Você também pode conectar seus dispositivos USB agora. 10 | image-view-title = Escolha uma imagem 11 | no-image-selected = Nenhuma imagem selecionada 12 | none = Nenhuma 13 | warning = Atenção: 14 | 15 | # Devices View 16 | device-too-small = Dispositivo muito pequeno 17 | devices-view-description = A gravação irá apagar todos os dados nos dispositivos selecionados. 18 | devices-view-title = Selecionar dispositivos 19 | select-all = Selecionar tudo 20 | 21 | # Flashing View 22 | flash-view-description = Não desconecte dispositivos enquanto eles estão sendo gravados. 23 | flash-view-title = Gravando dispositivos 24 | 25 | # Summary View 26 | flashing-completed = Gravação finalizada 27 | flashing-completed-with-errors = Gravação finalizada com erros 28 | flash-again = Gravar novamente 29 | 30 | # Error View 31 | critical-error = Ocorreu um erro crítico 32 | 33 | # Misc 34 | cancel = Cancelar 35 | close = Fechar 36 | done = Finalizado 37 | next = Próximo 38 | open = Abrir 39 | task-finished = Finalizado 40 | 41 | # Events 42 | error = erro: {$why} 43 | partial-flash = {$number} de {$total} dispositivos gravados com sucesso 44 | successful-flash = {$total} dispositivos gravados com sucesso 45 | win-isos-not-supported = ISOs do Windows não são suportadas no momento 46 | 47 | # Errors 48 | iso-open-failed = Falha ao abrir ISO 49 | no-value-found = nenhum valor encontrado 50 | -------------------------------------------------------------------------------- /i18n/es/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Aplicador de imágenes para USB 2 | 3 | # Images View 4 | cannot-select-directories = El selector de archivos no puede seleccionar directorios 5 | check-label = Comprobar 6 | choose-image-button = Elegir imagen 7 | generating-checksum = Generando suma de comprobación 8 | hash-label = Resumen: 9 | image-view-description = Seleccione la .iso o .img que quiera aplicar. Puede conectar sus unidades USB ahora. 10 | image-view-title = Elija una imagen 11 | no-image-selected = No se seleccionó ninguna imagen 12 | none = Ninguna 13 | warning = Atención: 14 | 15 | # Devices View 16 | device-too-small = El dispositivo es demasiado pequeño 17 | devices-view-description = La aplicación borrará todos los datos de las unidades seleccionadas. 18 | devices-view-title = Selección de unidades 19 | select-all = Seleccionar todo 20 | 21 | # Flashing View 22 | flash-view-description = No desconecte ningún dispositivo mientras la aplicación esté llevándose a cabo. 23 | flash-view-title = Aplicando la imagen en los dispositivos 24 | 25 | # Summary View 26 | flashing-completed = Finalizó la aplicación 27 | flash-again = Aplicar de nuevo 28 | 29 | # Error View 30 | critical-error = Se produjo un error grave 31 | 32 | # Misc 33 | cancel = Cancelar 34 | close = Cerrar 35 | next = Siguiente 36 | open = Abrir 37 | task-finished = Completado 38 | 39 | # Events 40 | error = error: {$why} 41 | partial-flash = Se aplicó correctamente la imagen en {$number} de {$total} dispositivos 42 | successful-flash = Se aplicó correctamente la imagen en {$total} dispositivos 43 | win-isos-not-supported = Por el momento no se admiten las ISO de Windows 44 | 45 | # Errors 46 | iso-open-failed = No se pudo abrir la ISO 47 | no-value-found = no se encontró ningún valor 48 | -------------------------------------------------------------------------------- /i18n/nl/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = usb-flasher 2 | 3 | # Images View 4 | cannot-select-directories = Er kunnen geen mappen worden gekozen 5 | check-label = Controleren 6 | choose-image-button = Kies een schijfkopie 7 | generating-checksum = Controlegetal wordt gegenereerd 8 | hash-label = Hash: 9 | image-view-description = Selecteer een .iso- of .img-bestand en koppel uw usb-schijven. 10 | image-view-title = Kies een schijfkopie 11 | no-image-selected = Geen schijfkopie gekozen 12 | none = Geen 13 | warning = Waarschuwing: 14 | 15 | # Devices View 16 | device-too-small = Het apparaat heeft onvoldoende ruimte 17 | devices-view-description = Door het maken van een opstartschijf, worden alle gegevens op de usb-schijf gewist. 18 | devices-view-title = Selecteer schijven 19 | select-all = Alle schijven selecteren 20 | 21 | # Flashing View 22 | flash-view-description = Koppel uw usb-schijven niet af zolang het proces loopt. 23 | flash-view-title = Schijven worden geflasht 24 | 25 | # Summary View 26 | flashing-completed = Flashen voltooid 27 | flashing-completed-with-errors = Flashen voltooid, maar met fouten 28 | flash-again = Opnieuw flashen 29 | 30 | # Error View 31 | critical-error = Er is een kritieke fout opgetreden 32 | 33 | # Misc 34 | cancel = Annuleren 35 | close = Sluiten 36 | done = Klaar 37 | next = Volgende 38 | open = Openen 39 | task-finished = Voltooid 40 | 41 | # Events 42 | error = Foutmelding: {$why} 43 | partial-flash = {$number} van {$total} opstartschijven gemaakt 44 | successful-flash = {$total} opstartschijven gemaakt 45 | win-isos-not-supported = ISO-bestanden van Windows worden momenteel niet ondersteund 46 | 47 | # Errors 48 | iso-open-failed = Het ISO-bestand kan niet worden geopend 49 | no-value-found = geen waarde aangetroffen 50 | -------------------------------------------------------------------------------- /i18n/sr/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Kreator USB instalacionog medijuma 2 | 3 | # Images View 4 | cannot-select-directories = Nemoguće je odabrati direktorijum 5 | check-label = Proveri 6 | choose-image-button = Izaberite instalacioni fajl 7 | generating-checksum = Generisanje kontrolne sume 8 | hash-label = Heš: 9 | image-view-description = Izaberite .iso ili .img koji želite da stavite na USB. Sada možete priključiti Vaš USB uređaj. 10 | image-view-title = Izaberite instalacioni fajl 11 | no-image-selected = Nijedan instalacioni fajl nije izabran 12 | none = Nijedan 13 | warning = Upozorenje: 14 | 15 | # Devices View 16 | device-too-small = Uređaj ima premalo prostora 17 | devices-view-description = Sav sadržaj izabranih USB uređaja će biti izbrisan. 18 | devices-view-title = Izaberite uređaje 19 | select-all = Izaberite sve 20 | 21 | # Flashing View 22 | flash-view-description = Ne isključujte USB uređaje dok je kreiranje instalacije u toku. 23 | flash-view-title = Kreiranje instalacionog medijuma 24 | 25 | # Summary View 26 | flashing-completed = Kreiranje instalacionog medijuma je završeno 27 | flash-again = Ponovno kreiranje instalacionog medijuma 28 | 29 | # Error View 30 | critical-error = Desila se kritična greška 31 | 32 | # Misc 33 | cancel = Izađi 34 | close = Zatvori 35 | done = Gotovo 36 | next = Dalje 37 | open = Otvori 38 | task-finished = Gotovo 39 | 40 | # Events 41 | error = greška: {$why} 42 | partial-flash = {$number} od {$total} uređaja su uspešno kreirani instalacioni uređaji 43 | successful-flash = {$total} uređaja su sada instalacioni uređaji 44 | win-isos-not-supported = Windows ISO fajlovi trenutno nisu podržani 45 | 46 | # Errors 47 | iso-open-failed = Greška pri otvaranju ISO fajla 48 | no-value-found = vrednost nije pronađena 49 | -------------------------------------------------------------------------------- /i18n/hi/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB फ़्लैशर 2 | 3 | # Images View 4 | cannot-select-directories = फ़ाइल चयनकर्ता फ़ोल्डर का चयन नहीं कर सकता। 5 | check-label = जांच करें 6 | choose-image-button = इमेज चुनें 7 | generating-checksum = चेकसम बना रहा है। 8 | hash-label = हैश: 9 | image-view-description = वह .iso या .img फ़ाइल का चयन करें जिसे आप फ्लैश करना चाहते हैं। आप अब अपने USB ड्राइव को भी प्लग कर सकते हैं। 10 | image-view-title = एक इमेज चुनें 11 | no-image-selected = कोई इमेज चयनित नहीं की गई है। 12 | none = कोई नहीं 13 | warning = ध्यान दें: 14 | 15 | # Devices View 16 | device-too-small = डिवाइस मेमोरी के दृष्टि से बहुत छोटी है। 17 | devices-view-description = फ्लैशिंग से चयनित ड्राइव्स पर सभी डेटा मिटा दिया जाएगा। 18 | devices-view-title = ड्राइव्स चयन करें 19 | select-all = सभी को चयन करें 20 | 21 | # Flashing View 22 | flash-view-description = डिवाइस को फ्लैश किए जा रहे हैं, इस दौरान उन्हें बिना बात के ना निकालें। 23 | flash-view-title = डिवाइस को फ्लैश कर रहा है। 24 | 25 | # Summary View 26 | flashing-completed = फ्लैशिंग पूर्ण हुई है। 27 | flashing-completed-with-errors = फ्लैशिंग पूर्ण हुई है, कुछ गड़बड़ीयों के साथ! 28 | flash-again = फिर से फ्लैश करें 29 | 30 | # Error View 31 | critical-error = गंभीर गड़बड़ी हुई है। 32 | 33 | # Misc 34 | cancel = रद्द करें 35 | close = बंद करें 36 | done = हो गया 37 | next = अगला 38 | open = खोलें 39 | task-finished = पूर्ण हुई है। 40 | 41 | # Events 42 | error = गड़बड़ी: {$why} 43 | partial-flash = {$number} में से {$total} डिवाइस सफलतापूर्वक फ्लैश किए गए हैं। 44 | successful-flash = सफलतापूर्वक {$total} डिवाइस फ्लैश किए गए हैं। 45 | win-isos-not-supported = विंडोज ISOs वर्तमान में समर्थित नहीं हैं। 46 | 47 | # Errors 48 | iso-open-failed = ISO खोलने में विफल रहा। 49 | no-value-found = कोई मूल्य नहीं मिला। 50 | -------------------------------------------------------------------------------- /i18n/it/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Creatore di immagini disco per USB 2 | 3 | # Images View 4 | cannot-select-directories = Il file manager non può selezionare le directory scelte 5 | check-label = Mostra 6 | choose-image-button = Scegli Immagine 7 | generating-checksum = Genera Checksum 8 | hash-label = Hash: 9 | image-view-description = Seleziona il file .iso o .img che desideri applicare. Ora puoi anche collegare le tue unità USB. 10 | image-view-title = Seleziona un'immagine 11 | no-image-selected = Nessuna immagine selezionata 12 | none = Nessuno 13 | warning = Attenzione: 14 | 15 | # Devices View 16 | device-too-small = Memoria dispositivo insufficiente 17 | devices-view-description = La creazione cancellerà tutti i dati sulle unità selezionate. 18 | devices-view-title = Seleziona unità 19 | select-all = Seleziona tutto 20 | 21 | # Flashing View 22 | flash-view-description = Non scollegare i dispositivi mentre vengono formattati. 23 | flash-view-title = Dispositivi Pronti 24 | 25 | # Summary View 26 | flashing-completed = Creazione unità USB Completata 27 | flashing-completed-with-errors = Creazione unità USB Completata con degli errori 28 | flash-again = Crea Ancora 29 | 30 | # Error View 31 | critical-error = Si è verificato un Errore Critico 32 | 33 | # Misc 34 | cancel = Annulla 35 | close = Chiudi 36 | done = Fatto 37 | next = Prossimo 38 | open = Apri 39 | task-finished = Completa 40 | 41 | # Events 42 | error = Errore: {$why} 43 | partial-flash = {$number} di {$total} unità USB create correttamente 44 | successful-flash = {$total} unità USB create correttamente 45 | win-isos-not-supported = Le ISO Windows non sono attualmente supportate 46 | 47 | # Errors 48 | iso-open-failed = Impossibile aprire ISO 49 | no-value-found = nessun valore trovato 50 | -------------------------------------------------------------------------------- /i18n/pt/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = Criar disco de arranque 2 | 3 | # Images View 4 | cannot-select-directories = O seletor de ficheiros não pode selecionar diretórios 5 | check-label = Verificar 6 | choose-image-button = Escolher Imagem 7 | generating-checksum = A gerar Checksum 8 | hash-label = Hash: 9 | image-view-description = Selecionar a .iso ou .img que deseja gravar. Pode também inserir agora as suas unidades USB. 10 | image-view-title = Escolha uma Imagem 11 | no-image-selected = Nenhuma imagem selecionada 12 | none = Nenhuma 13 | warning = Aviso: 14 | 15 | # Devices View 16 | device-too-small = Dispositivo demasiado pequeno 17 | devices-view-description = Durante a gravação todos os dados nas unidades selecionadas serão eliminados. 18 | devices-view-title = Selecionar Unidades 19 | select-all = Selecionar tudo 20 | 21 | # Flashing View 22 | flash-view-description = Não retire os dispositivos durante o processo de gravação. 23 | flash-view-title = Gravação em curso no dispositivo 24 | 25 | # Summary View 26 | flashing-completed = Processo de gravação concluído 27 | flashing-completed-with-errors = Processo de gravação concluído com erros 28 | flash-again = Repetir processo de gravação 29 | 30 | # Error View 31 | critical-error = Ocorreu um erro crítico 32 | 33 | # Misc 34 | cancel = Cancelar 35 | close = Fechar 36 | done = Terminado 37 | next = Seguinte 38 | open = Abrir 39 | task-finished = Concluído 40 | 41 | # Events 42 | error = erro: {$why} 43 | partial-flash = {$number} de {$total} dispositivos gravados com sucesso 44 | successful-flash = {$total} dispositivos gravados com sucesso 45 | win-isos-not-supported = As ISOs do Windows não são atualmente suportadas 46 | 47 | # Errors 48 | iso-open-failed = Falha ao abrir a ISO 49 | no-value-found = nenhum valor encontrado 50 | -------------------------------------------------------------------------------- /i18n/tr/popsicle_gtk.ftl: -------------------------------------------------------------------------------- 1 | app-title = USB Disk Görüntüsü Yazdırma 2 | 3 | # Images View 4 | cannot-select-directories = Klasör seçilemez 5 | check-label = Kontrol et 6 | choose-image-button = Disk görüntüsü seç 7 | generating-checksum = Disk görüntüsü özeti oluşturuluyor 8 | hash-label = Özet Kodu: 9 | image-view-description = USB belleğe yazdırmak istediğiniz .iso veya .img dosyasını seçin. USB belleğinizi şimdi takabilirsiniz. 10 | image-view-title = Disk görüntüsünü seçin 11 | no-image-selected = Disk görüntüsü seçilmedi 12 | none = Yok 13 | warning = Uyarı: 14 | 15 | # Devices View 16 | device-too-small = Bellek aygıtı çok küçük 17 | devices-view-description = Disk görüntüsü yazdırılırken seçilen bellek aygıtlarındaki bütün veriler silinecek. 18 | devices-view-title = Bellek aygıtlarını seçin 19 | select-all = Hepsini seç 20 | 21 | # Flashing View 22 | flash-view-description = Bellek aygıtlarını yazdırma işlemi süresince bilgisayardan çıkarmayın. 23 | flash-view-title = Disk görüntüsü bellek aygıtlarına yazdırılıyor. 24 | 25 | # Summary View 26 | flashing-completed = Yazdırma işlemi tamamlandı. 27 | flashing-completed-with-errors = Yazdırma işlemi hatalarla tamamlandı. 28 | flash-again = Tekrar yazdır 29 | 30 | # Error View 31 | critical-error = Kritik bir hata oluştu. 32 | 33 | # Misc 34 | cancel = İptal et 35 | close = Kapat 36 | done = Tamam 37 | next = Sonraki 38 | open = Aç 39 | task-finished = Tamamlandı 40 | 41 | # Events 42 | error = hata: {$why} 43 | partial-flash = Disk görüntüsü {$total} aygıtın {$number} tanesine başarıyla yazdırıldı. 44 | successful-flash = Disk görüntüsü {$total} aygıta başarıyla yazdırıldı. 45 | win-isos-not-supported = Windows ISO dosyaları henüz desteklenmiyor. 46 | 47 | # Errors 48 | iso-open-failed = ISO dosyası açılamadı. 49 | no-value-found = Değer bulunamadı. 50 | -------------------------------------------------------------------------------- /src/codec.rs: -------------------------------------------------------------------------------- 1 | use futures_codec::{BytesMut, Decoder}; 2 | use memchr::memchr; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{io, path::PathBuf}; 5 | use thiserror::Error; 6 | 7 | /// Errors that may occur when decoding the IPC stream. 8 | #[derive(Debug, Error)] 9 | pub enum Error { 10 | #[error("failed to decode popsicle message: {{\n {}\n}}", input)] 11 | Decode { input: Box, source: ron::de::SpannedError }, 12 | #[error("reading from popsicle stream failed")] 13 | Read(#[from] io::Error), 14 | } 15 | 16 | /// Popsicle's IPC protocol 17 | #[derive(Debug, Deserialize, PartialEq, Serialize)] 18 | pub enum Message { 19 | Device(PathBuf), 20 | Finished(PathBuf), 21 | Message(PathBuf, String), 22 | Set(PathBuf, u64), 23 | Size(u64), 24 | } 25 | 26 | /// A decoder for creating a stream of messages from a reader 27 | /// 28 | /// ```ignore 29 | /// use futures_code::FramedRead; 30 | /// 31 | /// FramedRead::new(pipe_reader, PopsicleDecoder::default()) 32 | /// ``` 33 | #[derive(Default)] 34 | pub struct PopsicleDecoder; 35 | 36 | impl Decoder for PopsicleDecoder { 37 | type Item = Message; 38 | type Error = Error; 39 | 40 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 41 | match memchr(b'\n', src) { 42 | Some(pos) => { 43 | let buf = src.split_to(pos + 1); 44 | match ron::de::from_bytes::(&buf) { 45 | Ok(value) => Ok(Some(value)), 46 | Err(source) => Err(Error::Decode { 47 | input: String::from_utf8_lossy(&buf).into_owned().into(), 48 | source, 49 | }), 50 | } 51 | } 52 | None => Ok(None), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/ipc.rs: -------------------------------------------------------------------------------- 1 | use futures::{executor, io::AllowStdIo, prelude::*}; 2 | use futures_codec::FramedRead; 3 | use popsicle::codec::*; 4 | use std::io::Cursor; 5 | 6 | const SAMPLE: &[u8] = include_bytes!("ipc.ron"); 7 | 8 | #[test] 9 | fn ipc() { 10 | executor::block_on(async move { 11 | let expected = vec![ 12 | Message::Size(2229190656), 13 | Message::Device("/dev/sdb".into()), 14 | Message::Device("/dev/sda".into()), 15 | Message::Set("/dev/sda".into(), 589824), 16 | Message::Set("/dev/sdb".into(), 589824), 17 | Message::Set("/dev/sdb".into(), 384434176), 18 | Message::Set("/dev/sda".into(), 1669005312), 19 | Message::Set("/dev/sdb".into(), 2228748288), 20 | Message::Set("/dev/sda".into(), 0), 21 | Message::Message("/dev/sda".into(), "S".into()), 22 | Message::Set("/dev/sdb".into(), 0), 23 | Message::Message("/dev/sdb".into(), "S".into()), 24 | Message::Set("/dev/sda".into(), 0), 25 | Message::Message("/dev/sda".into(), "V".into()), 26 | Message::Set("/dev/sdb".into(), 0), 27 | Message::Message("/dev/sdb".into(), "V".into()), 28 | Message::Finished("/dev/sda".into()), 29 | Message::Finished("/dev/sdb".into()), 30 | ]; 31 | 32 | let input = AllowStdIo::new(Cursor::new(SAMPLE)); 33 | 34 | let mut stream = FramedRead::new(input, PopsicleDecoder::default()); 35 | 36 | let mut expected_iter = expected.iter(); 37 | 38 | let mut matched = 0; 39 | while let Some(message) = stream.next().await { 40 | let message = message.unwrap(); 41 | 42 | assert_eq!(message, *expected_iter.next().unwrap()); 43 | matched += 1; 44 | } 45 | 46 | assert_eq!(matched, expected.len()); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /gtk/src/app/views/view.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | use gtk::{self, Align, Image, Label, Orientation}; 3 | 4 | pub struct View { 5 | pub container: gtk::Box, 6 | pub icon: Image, 7 | pub topic: Label, 8 | pub description: Label, 9 | pub panel: gtk::Box, 10 | } 11 | 12 | impl View { 13 | pub fn new( 14 | icon: &str, 15 | topic: &str, 16 | description: &str, 17 | configure_panel: F, 18 | ) -> View { 19 | let icon = Image::from_icon_name(Some(icon), gtk::IconSize::Dialog); 20 | icon.set_valign(Align::Start); 21 | 22 | let topic = cascade! { 23 | Label::new(Some(topic)); 24 | ..set_halign(Align::Start); 25 | ..style_context().add_class("h2"); 26 | ..set_margin_bottom(6); 27 | }; 28 | 29 | let description = cascade! { 30 | Label::new(Some(description)); 31 | ..set_line_wrap(true); 32 | ..set_xalign(0.0); 33 | ..style_context().add_class("desc"); 34 | ..set_margin_bottom(6); 35 | }; 36 | 37 | let left_panel = cascade! { 38 | gtk::Box::new(Orientation::Vertical, 0); 39 | ..add(&icon); 40 | ..style_context().add_class("left-panel"); 41 | }; 42 | 43 | let right_panel = cascade! { 44 | let panel = gtk::Box::new(Orientation::Vertical, 0); 45 | ..add(&topic); 46 | ..add(&description); 47 | ..style_context().add_class("right-panel"); 48 | configure_panel(&panel); 49 | }; 50 | 51 | View { 52 | container: cascade! { 53 | gtk::Box::new(Orientation::Horizontal, 12); 54 | ..pack_start(&left_panel, false, false, 0); 55 | ..pack_start(&right_panel, true, true, 0); 56 | }, 57 | icon, 58 | topic, 59 | description, 60 | panel: right_panel, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Popsicle 2 | 3 | Popsicle is a Linux utility for flashing multiple USB devices in parallel, written in [Rust](https://www.rust-lang.org/en-US/). 4 | 5 | ## Build Dependencies 6 | 7 | If building the GTK front end, you will be required to install the development dependencies for GTK and D-Bus, usually named `libgtk-3-dev` and `libdbus-1-dev`, respectively. No other dependencies are required to build the CLI or GTK front ends, besides Rust's `cargo` utility. 8 | 9 | For those who need to vendor Cargo's crate dependencies which are fetched from [Crates.io](https://crates.io/), you will need to install [cargo-vendor](https://github.com/alexcrichton/cargo-vendor), and then run `make vendor`. 10 | 11 | ## Installation Instructions 12 | 13 | A makefile is included for simply building and installing all required files into the system. You may either build both the CLI and GTK workspace, just the CLI workspace, or just the GTK workspace. 14 | 15 | - `make cli && sudo make install-cli` will build and install just the CLI workspace 16 | - `make gtk && sudo make install-gtk` will build and install just the GTK workspace 17 | - `make && sudo make install` will build and install both the CLI and GTK workspaces 18 | 19 | ## Screenshots 20 | 21 | ### Image Selection 22 | 23 | ![Image Selection](./screenshots/screenshot-01.png) 24 | 25 | ### Device Selection 26 | 27 | ![Device Selection](./screenshots/screenshot-02.png) 28 | 29 | The list will also dynamically refresh as devices are added and removed 30 | 31 | ![GIF Demo](./screenshots/device-monitoring.gif) 32 | 33 | ### Device Flashing 34 | 35 | ![Flashing Devices](./screenshots/screenshot-03.png) 36 | ![Flashing Devices](./screenshots/screenshot-04.png) 37 | 38 | ### Summary 39 | 40 | ![Summary](./screenshots/screenshot-05.png) 41 | 42 | ## Translators 43 | 44 | Translators are welcome to submit translations directly as a pull request to this project. It is generally expected that your pull requests will contain a single commit for each language that was added or improved, using a syntax like so: 45 | 46 | ``` 47 | i18n(eo): Add Esperanto language support 48 | ``` 49 | 50 | Translation files can be found [here](./i18n/). We are using [Project Fluent](https://projectfluent.org) for our translations, which should be easier than working with gettext. 51 | 52 | -------------------------------------------------------------------------------- /gtk/src/app/signals/images.rs: -------------------------------------------------------------------------------- 1 | use crate::app::events::{BackgroundEvent, UiEvent}; 2 | use crate::app::state::State; 3 | use crate::app::widgets::OpenDialog; 4 | use crate::app::{App, GtkUi}; 5 | use crate::misc; 6 | use gtk::prelude::*; 7 | use std::path::{Path, PathBuf}; 8 | 9 | impl App { 10 | pub fn connect_image_chooser(&self) { 11 | let state = self.state.clone(); 12 | let ui = self.ui.clone(); 13 | self.ui.content.image_view.chooser.connect_clicked(move |_| { 14 | if let Some(path) = OpenDialog::new(None).run() { 15 | let _ = state.ui_event_tx.send(UiEvent::SetImageLabel(path)); 16 | set_hash_widget(&state, &ui); 17 | } 18 | }); 19 | } 20 | 21 | pub fn connect_hash(&self) { 22 | let state = self.state.clone(); 23 | let ui = self.ui.clone(); 24 | self.ui.content.image_view.check.connect_clicked(move |_| { 25 | set_hash_widget(&state, &ui); 26 | }); 27 | } 28 | 29 | pub fn connect_image_drag_and_drop(&self) { 30 | let state = self.state.clone(); 31 | let ui = self.ui.clone(); 32 | let image_view = ui.content.image_view.view.container.clone(); 33 | 34 | misc::drag_and_drop(&image_view, move |data| { 35 | if let Some(uri) = data.text() { 36 | if uri.starts_with("file://") { 37 | let path = Path::new(&uri[7..uri.len() - 1]); 38 | if path.extension().map_or(false, |ext| ext == "iso" || ext == "img") 39 | && path.exists() 40 | { 41 | let _ = state.ui_event_tx.send(UiEvent::SetImageLabel(path.to_path_buf())); 42 | set_hash_widget(&state, &ui); 43 | } 44 | } 45 | } 46 | }); 47 | } 48 | } 49 | 50 | fn set_hash_widget(state: &State, ui: &GtkUi) { 51 | let hash = &ui.content.image_view.hash; 52 | 53 | let path = state.image_path.borrow(); 54 | let kind = match hash.active() { 55 | Some(1) => "SHA512", 56 | Some(2) => "SHA256", 57 | Some(3) => "SHA1", 58 | Some(4) => "MD5", 59 | Some(5) => "BLAKE2b", 60 | _ => return, 61 | }; 62 | 63 | ui.content.image_view.chooser_container.set_visible_child_name("checksum"); 64 | ui.content.image_view.set_hash_sensitive(false); 65 | 66 | let _ = state.back_event_tx.send(BackgroundEvent::GenerateHash(PathBuf::from(&*path), kind)); 67 | } 68 | -------------------------------------------------------------------------------- /gtk/src/app/state/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app::events::{self, BackgroundEvent, UiEvent}; 2 | use atomic::Atomic; 3 | use crossbeam_channel::{unbounded, Receiver, Sender}; 4 | use dbus_udisks2::DiskDevice; 5 | use libc; 6 | use std::cell::{Cell, RefCell}; 7 | use std::env; 8 | use std::fs::File; 9 | use std::path::PathBuf; 10 | use std::sync::Arc; 11 | 12 | #[derive(Clone, Copy, Debug, PartialEq)] 13 | pub enum ActiveView { 14 | Images, 15 | Devices, 16 | Flashing, 17 | Summary, 18 | Error, 19 | } 20 | 21 | pub struct State { 22 | pub ui_event_tx: Sender, 23 | pub ui_event_rx: Receiver, 24 | pub back_event_tx: Sender, 25 | 26 | pub active_view: Cell, 27 | 28 | pub image: RefCell>, 29 | pub image_path: RefCell, 30 | pub image_size: Arc>, 31 | 32 | pub available_devices: RefCell]>>, 33 | pub selected_devices: RefCell>>, 34 | } 35 | 36 | impl State { 37 | pub fn new() -> Self { 38 | let (back_event_tx, back_event_rx) = unbounded(); 39 | let (ui_event_tx, ui_event_rx) = unbounded(); 40 | 41 | // If running in pkexec or sudo, restore home directory for open dialog, 42 | // and then downgrade permissions back to a regular user. 43 | if let Ok(pkexec_uid) = env::var("PKEXEC_UID").or_else(|_| env::var("SUDO_UID")) { 44 | if let Ok(uid) = pkexec_uid.parse::() { 45 | if let Some(passwd) = pwd::Passwd::from_uid(uid) { 46 | env::set_var("HOME", passwd.dir); 47 | downgrade_permissions(passwd.uid, passwd.gid); 48 | } 49 | } 50 | } 51 | 52 | events::background_thread(ui_event_tx.clone(), back_event_rx); 53 | 54 | Self { 55 | ui_event_rx, 56 | ui_event_tx, 57 | back_event_tx, 58 | active_view: Cell::new(ActiveView::Images), 59 | image: RefCell::new(None), 60 | image_path: RefCell::new(PathBuf::new()), 61 | image_size: Arc::new(Atomic::new(0u64)), 62 | available_devices: RefCell::new(Box::new([])), 63 | selected_devices: RefCell::new(Vec::new()), 64 | } 65 | } 66 | } 67 | 68 | /// Downgrades the permissions of the current thread to the specified user and group ID. 69 | fn downgrade_permissions(uid: u32, gid: u32) { 70 | unsafe { 71 | libc::setresgid(gid, gid, gid); 72 | libc::setresuid(uid, uid, uid); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gtk/assets/com.system76.Popsicle.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.system76.Popsicle 5 | CC0-1.0 6 | MIT 7 | System76 8 | michael@system76.com 9 | https://github.com/pop-os/popsicle 10 | https://github.com/pop-os/popsicle 11 | Popsicle 12 | Flash multiple USB devices in parallel 13 | 14 |

Write an ISO or other image to multiple USB devices all at once. Easily preparing a bunch of flash drives of your favorite OS with just a couple of clicks.

15 |
    16 |
  • Supports USB 2 and 3 devices
  • 17 |
  • Use USB hubs for massively parallel writing
  • 18 |
  • Verify your image with the SHA256 or MD5 checksum
  • 19 |
  • Check the progress, speed, and success of each device while flashing
  • 20 |
  • Open ISO or IMG files from the app, or straight from your file manager
  • 21 |
22 |
23 | 24 | System 25 | 26 | com.system76.Popsicle.desktop 27 | https://raw.githubusercontent.com/pop-os/popsicle/master/gtk/assets/icons/512x512/apps/com.system76.Popsicle.png 28 | 29 | 30 | https://raw.githubusercontent.com/pop-os/popsicle/master/screenshots/screenshot-01.png 31 | 32 | 33 | https://raw.githubusercontent.com/pop-os/popsicle/master/screenshots/screenshot-02.png 34 | 35 | 36 | https://raw.githubusercontent.com/pop-os/popsicle/master/screenshots/screenshot-03.png 37 | 38 | 39 | https://raw.githubusercontent.com/pop-os/popsicle/master/screenshots/screenshot-04.png 40 | 41 | 42 | https://raw.githubusercontent.com/pop-os/popsicle/master/screenshots/screenshot-05.png 43 | 44 | 45 | 46 | 47 | application/x-cd-image 48 | application/x-raw-disk-image 49 | 50 | 51 | popsicle-gtk 52 | popsicle 53 | 54 | 55 | Pop!_OS 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default_prefix = /usr/local 2 | prefix ?= $(default_prefix) 3 | exec_prefix = $(prefix) 4 | bindir = $(exec_prefix)/bin 5 | libdir = $(exec_prefix)/lib 6 | includedir = $(prefix)/include 7 | datarootdir = $(prefix)/share 8 | datadir = $(datarootdir) 9 | 10 | CLI_SOURCES = $(shell find cli -type f -wholename '*src/*.rs') cli/Cargo.toml 11 | GTK_SOURCES = $(shell find gtk -type f -wholename '*src/*.rs') gtk/Cargo.toml 12 | SHR_SOURCES = $(shell find src -type f -wholename '*src/*.rs') Cargo.toml Cargo.lock 13 | 14 | RELEASE = debug 15 | DEBUG ?= 0 16 | ifeq (0,$(DEBUG)) 17 | ARGS = --release 18 | RELEASE = release 19 | endif 20 | 21 | VENDORED ?= 0 22 | ifeq (1,$(VENDORED)) 23 | ARGS += --frozen 24 | endif 25 | 26 | TARGET = target/$(RELEASE) 27 | 28 | .PHONY: all clean distclean install uninstall update 29 | 30 | BIN=popsicle 31 | APPID=com.system76.Popsicle 32 | APPDATA=$(APPID).appdata.xml 33 | DESKTOP=$(APPID).desktop 34 | GTK_BIN=popsicle-gtk 35 | ICONS=\ 36 | 512x512/apps/$(APPID).png \ 37 | 16x16@2x/apps/$(APPID).png \ 38 | 32x32@2x/apps/$(APPID).png \ 39 | 32x32/apps/$(APPID).png \ 40 | 48x48@2x/apps/$(APPID).png \ 41 | 24x24/apps/$(APPID).png \ 42 | 48x48/apps/$(APPID).png \ 43 | 16x16/apps/$(APPID).png \ 44 | 24x24@2x/apps/$(APPID).png \ 45 | 512x512@2x/apps/$(APPID).png 46 | 47 | all: cli gtk 48 | 49 | cli: $(TARGET)/$(BIN) $(TARGET)/$(BIN).1.gz $(CLI_SOURCES) $(SHR_SOURCES) 50 | 51 | gtk: $(TARGET)/$(GTK_BIN) $(GTK_SOURCES) $(SHR_SOURCES) 52 | 53 | clean: 54 | cargo clean 55 | 56 | distclean: clean 57 | rm -rf .cargo vendor vendor.tar 58 | 59 | vendor: vendor.tar 60 | 61 | vendor.tar: 62 | mkdir -p .cargo 63 | cargo vendor | head -n -1 > .cargo/config 64 | echo 'directory = "vendor"' >> .cargo/config 65 | tar pcf vendor.tar vendor 66 | rm -rf vendor 67 | 68 | install-cli: cli 69 | install -Dm 0755 "$(TARGET)/$(BIN)" "$(DESTDIR)$(bindir)/$(BIN)" 70 | install -Dm 0644 "$(TARGET)/$(BIN).1.gz" "$(DESTDIR)$(datadir)/man/man1/$(BIN).1.gz" 71 | 72 | install-gtk: gtk 73 | install -Dm 0755 "$(TARGET)/$(GTK_BIN)" "$(DESTDIR)$(bindir)/$(GTK_BIN)" 74 | install -Dm 0644 "gtk/assets/$(DESKTOP)" "$(DESTDIR)$(datadir)/applications/$(DESKTOP)" 75 | install -Dm 0644 "gtk/assets/$(APPDATA)" "$(DESTDIR)$(datadir)/metainfo/$(APPDATA)" 76 | for icon in $(ICONS); do \ 77 | install -D -m 0644 "gtk/assets/icons/$$icon" "$(DESTDIR)$(datadir)/icons/hicolor/$$icon"; \ 78 | done 79 | 80 | install: all install-cli install-gtk 81 | 82 | uninstall-cli: 83 | rm -f "$(DESTDIR)$(bindir)/$(BIN)" 84 | rm -f "$(DESTDIR)$(datadir)/man/man1/$(BIN).1.gz" 85 | 86 | uninstall-gtk: 87 | rm -f "$(DESTDIR)$(bindir)/$(GTK_BIN)" 88 | rm -f "$(DESTDIR)$(datadir)/applications/$(DESKTOP)" 89 | for icon in $(ICONS); do \ 90 | rm -f "$(DESTDIR)$(datadir)/icons/hicolor/$$icon"; \ 91 | done 92 | 93 | uninstall: uninstall-cli uninstall-gtk 94 | 95 | update: 96 | cargo update 97 | 98 | extract: 99 | ifeq ($(VENDORED),1) 100 | tar pxf vendor.tar 101 | endif 102 | 103 | $(TARGET)/$(BIN): extract 104 | cargo build --manifest-path cli/Cargo.toml $(ARGS) 105 | 106 | $(TARGET)/$(GTK_BIN): extract 107 | cargo build --manifest-path gtk/Cargo.toml $(ARGS) 108 | 109 | $(TARGET)/$(BIN).1.gz: $(TARGET)/$(BIN) 110 | help2man --no-info $< | gzip -c > $@.partial 111 | mv $@.partial $@ 112 | -------------------------------------------------------------------------------- /gtk/assets/process-completed-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | Pop Symbolic Icon Theme 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Pop Symbolic Icon Theme 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /gtk/src/app/events/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::flash::{FlashError, FlashRequest}; 2 | use crate::hash::hasher; 3 | 4 | use blake2::Blake2b512; 5 | use crossbeam_channel::{Receiver, Sender}; 6 | use dbus_udisks2::{DiskDevice, Disks, UDisks2}; 7 | use md5::Md5; 8 | use sha1::Sha1; 9 | use sha2::Sha256; 10 | use sha2::Sha512; 11 | use std::collections::HashMap; 12 | use std::io; 13 | use std::path::PathBuf; 14 | use std::sync::Arc; 15 | use std::thread::{self, JoinHandle}; 16 | 17 | pub type FlashResult = anyhow::Result<(anyhow::Result<()>, Vec>)>; 18 | 19 | pub enum UiEvent { 20 | SetImageLabel(PathBuf), 21 | RefreshDevices(Box<[Arc]>), 22 | SetHash(io::Result), 23 | Flash(JoinHandle), 24 | Reset, 25 | } 26 | 27 | pub enum BackgroundEvent { 28 | GenerateHash(PathBuf, &'static str), 29 | Flash(FlashRequest), 30 | RefreshDevices, 31 | } 32 | 33 | pub fn background_thread(events_tx: Sender, events_rx: Receiver) { 34 | thread::spawn(move || { 35 | let mut hashed: HashMap<(PathBuf, &'static str), String> = HashMap::new(); 36 | 37 | let mut device_paths = Vec::new(); 38 | 39 | loop { 40 | match events_rx.recv() { 41 | Ok(BackgroundEvent::GenerateHash(path, kind)) => { 42 | // Check if the cache already contains this hash, and return it. 43 | if let Some(result) = hashed.get(&(path.clone(), kind)) { 44 | let _ = events_tx.send(UiEvent::SetHash(Ok(result.clone()))); 45 | continue; 46 | } 47 | 48 | // Hash the file at the given path. 49 | let result = match kind { 50 | "MD5" => hasher::(&path), 51 | "SHA256" => hasher::(&path), 52 | "SHA1" => hasher::(&path), 53 | "SHA512" => hasher::(&path), 54 | "BLAKE2b" => hasher::(&path), 55 | _ => Err(io::Error::new( 56 | io::ErrorKind::InvalidInput, 57 | "hash kind not supported", 58 | )), 59 | }; 60 | 61 | // If successful, cache the result. 62 | if let Ok(ref result) = result { 63 | hashed.insert((path.clone(), kind), result.clone()); 64 | } 65 | 66 | // Send this result back to the main thread. 67 | let _ = events_tx.send(UiEvent::SetHash(result)); 68 | } 69 | Ok(BackgroundEvent::RefreshDevices) => { 70 | // Fetch the current list of USB devices from popsicle. 71 | match refresh_devices() { 72 | Ok(devices) => { 73 | let new_device_paths: Vec<_> = 74 | devices.iter().map(|d| d.drive.path.clone()).collect(); 75 | if new_device_paths != device_paths { 76 | device_paths = new_device_paths; 77 | let _ = events_tx.send(UiEvent::RefreshDevices(devices)); 78 | } 79 | } 80 | Err(why) => eprintln!("failed to refresh devices: {}", why), 81 | } 82 | } 83 | Ok(BackgroundEvent::Flash(request)) => { 84 | let _ = events_tx.send(UiEvent::Flash( 85 | thread::Builder::new() 86 | .stack_size(10 * 1024 * 1024) 87 | .spawn(|| request.write()) 88 | .unwrap(), 89 | )); 90 | } 91 | Err(_) => break, 92 | } 93 | } 94 | }); 95 | } 96 | 97 | fn refresh_devices() -> anyhow::Result]>> { 98 | let udisks = UDisks2::new()?; 99 | let devices = Disks::new(&udisks).devices; 100 | let mut devices = devices 101 | .into_iter() 102 | .filter(|d| d.drive.connection_bus == "usb" || d.drive.connection_bus == "sdio") 103 | .filter(|d| d.parent.size != 0) 104 | .map(Arc::new) 105 | .collect::>() 106 | .into_boxed_slice(); 107 | devices.sort_by_key(|d| d.drive.id.clone()); 108 | Ok(devices) 109 | } 110 | -------------------------------------------------------------------------------- /gtk/src/app/views/devices.rs: -------------------------------------------------------------------------------- 1 | use super::View; 2 | use crate::fl; 3 | use crate::misc; 4 | use dbus_udisks2::DiskDevice; 5 | use gtk; 6 | use gtk::prelude::*; 7 | use std::cell::{Cell, RefCell}; 8 | use std::rc::Rc; 9 | use std::sync::Arc; 10 | 11 | use bytesize; 12 | 13 | type ViewReadySignal = Rc>>; 14 | 15 | pub struct DevicesView { 16 | pub view: View, 17 | pub list: gtk::ListBox, 18 | pub select_all: gtk::CheckButton, 19 | view_ready: ViewReadySignal, 20 | } 21 | 22 | impl DevicesView { 23 | pub fn new() -> DevicesView { 24 | let list = cascade! { 25 | gtk::ListBox::new(); 26 | ..style_context().add_class("frame"); 27 | ..style_context().add_class("devices"); 28 | ..set_hexpand(true); 29 | ..set_vexpand(true); 30 | }; 31 | 32 | let list_ = list.clone(); 33 | let select_all = cascade! { 34 | gtk::CheckButton::with_label(&fl!("select-all")); 35 | ..set_margin_start(4); 36 | ..set_margin_bottom(3); 37 | ..connect_toggled(move |all| { 38 | let state = all.is_active(); 39 | 40 | for row in list_.children() { 41 | if let Ok(row) = row.downcast::() { 42 | if let Some(widget) = row.children().first() { 43 | if let Some(button) = widget.downcast_ref::() { 44 | button.set_active(button.get_sensitive() && state); 45 | } 46 | } 47 | } 48 | } 49 | }); 50 | }; 51 | 52 | let list_box = cascade! { 53 | gtk::Box::new(gtk::Orientation::Vertical, 0); 54 | ..add(&select_all); 55 | ..add(&list); 56 | }; 57 | 58 | let select_scroller = cascade! { 59 | gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); 60 | ..set_hexpand(true); 61 | ..set_vexpand(true); 62 | ..add(&list_box); 63 | }; 64 | 65 | let view = View::new( 66 | "drive-removable-media-usb", 67 | &fl!("devices-view-title"), 68 | &fl!("devices-view-description"), 69 | |right_panel| right_panel.add(&select_scroller), 70 | ); 71 | 72 | let view_ready: ViewReadySignal = Rc::new(RefCell::new(Box::new(|_| ()))); 73 | 74 | DevicesView { view, list, select_all, view_ready } 75 | } 76 | 77 | pub fn get_buttons(&self) -> impl Iterator { 78 | self.list 79 | .children() 80 | .into_iter() 81 | .filter_map(|row| row.downcast::().ok()) 82 | .filter_map(|row| row.children().first().cloned()) 83 | .filter_map(|row| row.downcast::().ok()) 84 | } 85 | 86 | pub fn is_active_ids(&self) -> impl Iterator { 87 | self.get_buttons() 88 | .enumerate() 89 | .filter_map(|(id, button)| if button.is_active() { Some(id) } else { None }) 90 | } 91 | 92 | pub fn refresh(&self, devices: &[Arc], image_size: u64) { 93 | self.list.foreach(|w| self.list.remove(w)); 94 | 95 | let nselected = Rc::new(Cell::new(0)); 96 | 97 | for device in devices { 98 | let valid_size = device.parent.size >= image_size; 99 | 100 | let label = &misc::device_label(device); 101 | 102 | let size_str = bytesize::to_string(device.parent.size, true); 103 | let name = if valid_size { 104 | format!("{}\n{}", label, size_str) 105 | } else { 106 | let too_small = fl!("device-too-small"); 107 | format!("{}\n{}: {}", label, size_str, too_small) 108 | }; 109 | 110 | let view_ready = self.view_ready.clone(); 111 | let nselected = nselected.clone(); 112 | 113 | let row = cascade! { 114 | gtk::CheckButton::new(); 115 | ..set_sensitive(valid_size); 116 | ..add(&cascade! { 117 | gtk::Label::new(Some(name.as_str())); 118 | ..set_use_markup(true); 119 | }); 120 | ..connect_toggled(move |button| { 121 | if button.is_active() { 122 | nselected.set(nselected.get() + 1); 123 | } else { 124 | nselected.set(nselected.get() - 1); 125 | } 126 | 127 | (*view_ready.borrow())(nselected.get() != 0); 128 | }); 129 | }; 130 | self.list.insert(&row, -1); 131 | } 132 | 133 | self.list.show_all(); 134 | } 135 | 136 | pub fn reset(&self) { 137 | self.select_all.set_active(false); 138 | self.get_buttons().for_each(|c| c.set_active(false)); 139 | } 140 | 141 | pub fn connect_view_ready(&self, func: F) { 142 | *self.view_ready.borrow_mut() = Box::new(func); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use async_std::{fs::File, prelude::*}; 3 | use srmw::*; 4 | use std::{collections::HashMap, io::SeekFrom, time::Instant}; 5 | 6 | pub trait Progress { 7 | type Device; 8 | fn message(&mut self, device: &Self::Device, kind: &str, message: &str); 9 | fn finish(&mut self); 10 | fn set(&mut self, value: u64); 11 | } 12 | 13 | #[derive(new)] 14 | pub struct Task { 15 | image: File, 16 | 17 | #[new(default)] 18 | pub writer: MultiWriter, 19 | 20 | #[new(default)] 21 | pub state: HashMap, 22 | 23 | #[new(value = "125")] 24 | pub millis_between: u64, 25 | 26 | check: bool, 27 | } 28 | 29 | impl Task

{ 30 | /// Performs the asynchronous USB device flashing. 31 | pub async fn process(mut self, buf: &mut [u8]) -> anyhow::Result<()> { 32 | self.copy(buf).await.context("failed to copy ISO")?; 33 | 34 | if self.check { 35 | self.seek().await.context("failed to seek devices to start")?; 36 | self.validate(buf).await.context("validation error")?; 37 | } 38 | 39 | for (_, pb) in self.state.values_mut() { 40 | pb.finish(); 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | pub fn subscribe(&mut self, file: File, device: P::Device, progress: P) -> &mut Self { 47 | let entity = self.writer.insert(file); 48 | self.state.insert(entity, (device, progress)); 49 | self 50 | } 51 | 52 | async fn copy(&mut self, buf: &mut [u8]) -> anyhow::Result<()> { 53 | let mut stream = self.writer.copy(&mut self.image, buf); 54 | let mut total = 0; 55 | let mut last = Instant::now(); 56 | while let Some(event) = stream.next().await { 57 | match event { 58 | CopyEvent::Progress(written) => { 59 | total += written as u64; 60 | let now = Instant::now(); 61 | if now.duration_since(last).as_millis() > self.millis_between as u128 { 62 | last = now; 63 | for (_, pb) in self.state.values_mut() { 64 | pb.set(total); 65 | } 66 | } 67 | } 68 | CopyEvent::Failure(entity, why) => { 69 | let (device, mut pb) = self.state.remove(&entity).expect("missing entity"); 70 | pb.message(&device, "E", &format!("{}", why)); 71 | pb.finish(); 72 | } 73 | CopyEvent::SourceFailure(why) => { 74 | for (device, pb) in self.state.values_mut() { 75 | pb.message(device, "E", &format!("{}", why)); 76 | pb.finish(); 77 | } 78 | 79 | return Err(why).context("error reading from source"); 80 | } 81 | CopyEvent::NoWriters => return Err(anyhow!("no writers left")), 82 | } 83 | } 84 | 85 | Ok(()) 86 | } 87 | 88 | async fn seek(&mut self) -> anyhow::Result<()> { 89 | for (path, pb) in self.state.values_mut() { 90 | pb.set(0); 91 | pb.message(path, "S", ""); 92 | } 93 | 94 | self.image.seek(SeekFrom::Start(0)).await?; 95 | 96 | let mut stream = self.writer.seek(SeekFrom::Start(0)); 97 | while let Some((entity, why)) = stream.next().await { 98 | let (path, mut pb) = self.state.remove(&entity).expect("missing entity"); 99 | pb.message(&path, "E", &format!("errored seeking to start: {}", why)); 100 | pb.finish(); 101 | } 102 | 103 | Ok(()) 104 | } 105 | 106 | async fn validate(&mut self, buf: &mut [u8]) -> anyhow::Result<()> { 107 | for (path, pb) in self.state.values_mut() { 108 | pb.set(0); 109 | pb.message(path, "V", ""); 110 | } 111 | 112 | let copy_bufs = &mut Vec::new(); 113 | let mut total = 0; 114 | let mut stream = self.writer.validate(&mut self.image, buf, copy_bufs); 115 | 116 | while let Some(event) = stream.next().await { 117 | match event { 118 | ValidationEvent::Progress(written) => { 119 | total += written as u64; 120 | for (_, pb) in self.state.values_mut() { 121 | pb.set(total); 122 | } 123 | } 124 | ValidationEvent::Failure(entity, why) => { 125 | let (path, mut pb) = self.state.remove(&entity).expect("missing entity"); 126 | pb.message(&path, "E", &format!("{}", why)); 127 | pb.finish(); 128 | } 129 | ValidationEvent::SourceFailure(why) => { 130 | for (path, pb) in self.state.values_mut() { 131 | pb.message(path, "E", &format!("error reading from source: {}", why)); 132 | pb.finish(); 133 | } 134 | 135 | return Err(why).context("error reading from source"); 136 | } 137 | ValidationEvent::NoWriters => return Err(anyhow!("no writers left")), 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /gtk/src/flash.rs: -------------------------------------------------------------------------------- 1 | use crate::app::events::FlashResult; 2 | use atomic::Atomic; 3 | use dbus::arg::{OwnedFd, RefArg, Variant}; 4 | use dbus::blocking::{Connection, Proxy}; 5 | use dbus_udisks2::DiskDevice; 6 | use futures::executor; 7 | use popsicle::{Progress, Task}; 8 | use std::cell::Cell; 9 | use std::collections::HashMap; 10 | use std::fmt::{self, Debug, Display, Formatter}; 11 | use std::fs::File; 12 | use std::os::unix::io::FromRawFd; 13 | use std::str; 14 | use std::sync::atomic::Ordering; 15 | use std::sync::{Arc, Mutex}; 16 | use std::time::Duration; 17 | 18 | type UDisksOptions = HashMap<&'static str, Variant>>; 19 | 20 | #[derive(Clone, Copy, PartialEq)] 21 | pub enum FlashStatus { 22 | Inactive, 23 | Active, 24 | Killing, 25 | } 26 | 27 | unsafe impl bytemuck::NoUninit for FlashStatus {} 28 | 29 | pub struct FlashRequest { 30 | source: Option, 31 | destinations: Vec>, 32 | status: Arc>, 33 | progress: Arc>>, 34 | finished: Arc>>, 35 | } 36 | 37 | pub struct FlashTask { 38 | pub progress: Arc>>, 39 | pub previous: Arc>>, 40 | pub finished: Arc>>, 41 | } 42 | 43 | struct FlashProgress<'a> { 44 | request: &'a FlashRequest, 45 | id: usize, 46 | errors: &'a [Cell>], 47 | } 48 | 49 | #[derive(Clone, Debug)] 50 | pub struct FlashError { 51 | kind: String, 52 | message: String, 53 | } 54 | 55 | impl Display for FlashError { 56 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 57 | write!(f, "{}: {}", self.kind, self.message) 58 | } 59 | } 60 | 61 | impl std::error::Error for FlashError {} 62 | 63 | impl<'a> Progress for FlashProgress<'a> { 64 | type Device = (); 65 | 66 | fn message(&mut self, _device: &(), kind: &str, message: &str) { 67 | self.errors[self.id] 68 | .set(Err(FlashError { kind: kind.to_string(), message: message.to_string() })); 69 | } 70 | 71 | fn finish(&mut self) { 72 | self.request.finished[self.id].store(true, Ordering::SeqCst); 73 | } 74 | 75 | fn set(&mut self, value: u64) { 76 | self.request.progress[self.id].store(value, Ordering::SeqCst); 77 | } 78 | } 79 | 80 | impl FlashRequest { 81 | pub fn new( 82 | source: File, 83 | destinations: Vec>, 84 | status: Arc>, 85 | progress: Arc>>, 86 | finished: Arc>>, 87 | ) -> FlashRequest { 88 | FlashRequest { source: Some(source), destinations, status, progress, finished } 89 | } 90 | 91 | pub fn write(mut self) -> FlashResult { 92 | self.status.store(FlashStatus::Active, Ordering::SeqCst); 93 | 94 | let source = self.source.take().unwrap(); 95 | let res = self.write_inner(source); 96 | 97 | for atomic in self.finished.iter() { 98 | atomic.store(true, Ordering::SeqCst); 99 | } 100 | 101 | self.status.store(FlashStatus::Inactive, Ordering::SeqCst); 102 | 103 | res 104 | } 105 | 106 | fn write_inner(&self, source: File) -> FlashResult { 107 | // Unmount the devices beforehand. 108 | for device in &self.destinations { 109 | let _ = udisks_unmount(&device.parent.path); 110 | for partition in &device.partitions { 111 | let _ = udisks_unmount(&partition.path); 112 | } 113 | } 114 | 115 | // Then open them for writing to. 116 | let mut files = Vec::new(); 117 | for device in &self.destinations { 118 | let file = udisks_open(&device.parent.path)?; 119 | files.push(file); 120 | } 121 | 122 | let mut errors = vec![Ok(()); files.len()]; 123 | let errors_cells = Cell::from_mut(&mut errors as &mut [_]).as_slice_of_cells(); 124 | 125 | // How many bytes to write at a given time. 126 | let mut bucket = [0u8; 64 * 1024]; 127 | 128 | let mut task = Task::new(source.into(), false); 129 | for (i, file) in files.into_iter().enumerate() { 130 | let progress = FlashProgress { request: self, errors: errors_cells, id: i }; 131 | task.subscribe(file.into(), (), progress); 132 | } 133 | 134 | let res = executor::block_on(task.process(&mut bucket)); 135 | 136 | Ok((res, errors)) 137 | } 138 | } 139 | 140 | fn udisks_unmount(dbus_path: &str) -> anyhow::Result<()> { 141 | let connection = Connection::new_system()?; 142 | 143 | let dbus_path = ::dbus::strings::Path::new(dbus_path).map_err(anyhow::Error::msg)?; 144 | 145 | let proxy = Proxy::new("org.freedesktop.UDisks2", dbus_path, Duration::new(25, 0), &connection); 146 | 147 | let mut options = UDisksOptions::new(); 148 | options.insert("force", Variant(Box::new(true))); 149 | let res: Result<(), _> = 150 | proxy.method_call("org.freedesktop.UDisks2.Filesystem", "Unmount", (options,)); 151 | 152 | if let Err(err) = res { 153 | if err.name() != Some("org.freedesktop.UDisks2.Error.NotMounted") { 154 | return Err(anyhow::Error::new(err)); 155 | } 156 | } 157 | 158 | Ok(()) 159 | } 160 | 161 | fn udisks_open(dbus_path: &str) -> anyhow::Result { 162 | let connection = Connection::new_system()?; 163 | 164 | let dbus_path = ::dbus::strings::Path::new(dbus_path).map_err(anyhow::Error::msg)?; 165 | 166 | let proxy = 167 | Proxy::new("org.freedesktop.UDisks2", &dbus_path, Duration::new(25, 0), &connection); 168 | 169 | let mut options = UDisksOptions::new(); 170 | options.insert("flags", Variant(Box::new(libc::O_SYNC))); 171 | let res: (OwnedFd,) = 172 | proxy.method_call("org.freedesktop.UDisks2.Block", "OpenDevice", ("rw", options))?; 173 | 174 | Ok(unsafe { File::from_raw_fd(res.0.into_fd()) }) 175 | } 176 | -------------------------------------------------------------------------------- /gtk/src/app/views/images.rs: -------------------------------------------------------------------------------- 1 | use super::View; 2 | use crate::fl; 3 | use bytesize; 4 | use gtk::prelude::*; 5 | use gtk::*; 6 | use pango::{AttrColor, AttrList, EllipsizeMode}; 7 | use std::path::Path; 8 | 9 | pub struct ImageView { 10 | pub view: View, 11 | pub check: Button, 12 | pub chooser_container: Stack, 13 | pub chooser: Button, 14 | pub image_path: Label, 15 | pub hash: ComboBoxText, 16 | pub hash_label: Entry, 17 | } 18 | 19 | impl ImageView { 20 | pub fn new() -> ImageView { 21 | let chooser = cascade! { 22 | Button::with_label(&fl!("choose-image-button")); 23 | ..set_halign(Align::Center); 24 | ..set_margin_bottom(6); 25 | }; 26 | 27 | let image_label = format!("{}", fl!("no-image-selected")); 28 | 29 | let image_path = cascade! { 30 | Label::new(Some(&image_label)); 31 | ..set_use_markup(true); 32 | ..set_justify(Justification::Center); 33 | ..set_ellipsize(EllipsizeMode::End); 34 | }; 35 | 36 | let button_box = cascade! { 37 | Box::new(Orientation::Vertical, 0); 38 | ..pack_start(&chooser, false, false, 0); 39 | ..pack_start(&image_path, false, false, 0); 40 | }; 41 | 42 | let spinner = Spinner::new(); 43 | spinner.start(); 44 | 45 | let spinner_label = cascade! { 46 | Label::new(Some(&fl!("generating-checksum"))); 47 | ..style_context().add_class("bold"); 48 | }; 49 | 50 | let spinner_box = cascade! { 51 | Box::new(Orientation::Vertical, 0); 52 | ..pack_start(&spinner, false, false, 0); 53 | ..pack_start(&spinner_label, false, false, 0); 54 | }; 55 | 56 | let hash = cascade! { 57 | ComboBoxText::new(); 58 | ..append_text(&fl!("none")); 59 | ..append_text("SHA512"); 60 | ..append_text("SHA256"); 61 | ..append_text("SHA1"); 62 | ..append_text("MD5"); 63 | ..append_text("BLAKE2b"); 64 | ..set_active(Some(0)); 65 | ..set_sensitive(false); 66 | }; 67 | 68 | let hash_label = cascade! { 69 | Entry::new(); 70 | ..set_sensitive(false); 71 | }; 72 | 73 | let label = cascade! { 74 | Label::new(Some(&fl!("hash-label"))); 75 | ..set_margin_end(6); 76 | }; 77 | 78 | let check = cascade! { 79 | Button::with_label(&fl!("check-label")); 80 | ..style_context().add_class(&STYLE_CLASS_SUGGESTED_ACTION); 81 | ..set_sensitive(false); 82 | }; 83 | 84 | let hash_label_clone = hash_label.clone(); 85 | let check_clone = check.clone(); 86 | hash.connect_changed(move |combo_box| { 87 | let sensitive = combo_box.active_text().is_some_and(|text| text.as_str() != "None"); 88 | 89 | hash_label_clone.set_sensitive(sensitive); 90 | check_clone.set_sensitive(sensitive); 91 | }); 92 | 93 | let combo_container = cascade! { 94 | Box::new(Orientation::Horizontal, 0); 95 | ..add(&hash); 96 | ..pack_start(&hash_label, true, true, 0); 97 | ..style_context().add_class("linked"); 98 | }; 99 | 100 | let hash_container = cascade! { 101 | let tmp = Box::new(Orientation::Horizontal, 0); 102 | ..pack_start(&label, false, false, 0); 103 | ..pack_start(&combo_container, true, true, 0); 104 | ..pack_start(&check, false, false, 0); 105 | ..set_border_width(6); 106 | }; 107 | 108 | let chooser_container = cascade! { 109 | Stack::new(); 110 | ..add_named(&button_box, "chooser"); 111 | ..add_named(&spinner_box, "checksum"); 112 | ..set_visible_child_name("chooser"); 113 | ..set_margin_top(12); 114 | ..set_margin_bottom(24); 115 | }; 116 | 117 | let view = View::new( 118 | "application-x-cd-image", 119 | &fl!("image-view-title"), 120 | &fl!("image-view-description"), 121 | |right_panel| { 122 | right_panel.pack_start(&chooser_container, true, false, 0); 123 | right_panel.pack_start(&hash_container, false, false, 0); 124 | }, 125 | ); 126 | 127 | ImageView { view, check, chooser_container, chooser, image_path, hash, hash_label } 128 | } 129 | 130 | pub fn set_hash_sensitive(&self, sensitive: bool) { 131 | self.hash.set_sensitive(sensitive); 132 | } 133 | 134 | pub fn set_hash(&self, hash: &str) { 135 | let text = self.hash_label.text(); 136 | if !text.is_empty() { 137 | let fg = if text.eq_ignore_ascii_case(hash) { 138 | AttrColor::new_foreground(0, u16::MAX, 0) 139 | } else { 140 | AttrColor::new_foreground(u16::MAX, 0, 0) 141 | }; 142 | let attrs = AttrList::new(); 143 | attrs.insert(fg); 144 | self.hash_label.set_attributes(&attrs); 145 | } else { 146 | self.hash_label.set_text(hash); 147 | } 148 | } 149 | 150 | pub fn set_image(&self, path: &Path, size: u64, warning: Option<&str>) { 151 | let size_str = bytesize::to_string(size, true); 152 | let mut label: String = match path.file_name() { 153 | Some(name) => format!("{}\n{}", name.to_string_lossy(), size_str), 154 | None => format!("{}", fl!("cannot-select-directories")), 155 | }; 156 | 157 | if let Some(warning) = warning { 158 | let subject = fl!("warning"); 159 | label += &format!("\n{}: {}", subject, warning); 160 | }; 161 | 162 | self.image_path.set_markup(&label); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate anyhow; 3 | #[macro_use] 4 | extern crate derive_new; 5 | #[macro_use] 6 | extern crate thiserror; 7 | 8 | pub extern crate mnt; 9 | 10 | pub mod codec; 11 | 12 | mod task; 13 | 14 | pub use self::task::{Progress, Task}; 15 | 16 | use anyhow::Context; 17 | use as_result::MapResult; 18 | use async_std::{ 19 | fs::{self, File, OpenOptions}, 20 | os::unix::fs::OpenOptionsExt, 21 | path::{Path, PathBuf}, 22 | }; 23 | use futures::{executor, prelude::*}; 24 | use mnt::MountEntry; 25 | use std::{ 26 | io, 27 | os::unix::{ffi::OsStrExt, fs::FileTypeExt}, 28 | process::Command, 29 | }; 30 | use usb_disk_probe::stream::UsbDiskProbe; 31 | 32 | #[derive(Debug, Error)] 33 | #[rustfmt::skip] 34 | pub enum ImageError { 35 | #[error("image could not be opened: {}", why)] 36 | Open { why: io::Error }, 37 | #[error("unable to get image metadata: {}", why)] 38 | Metadata { why: io::Error }, 39 | #[error("image was not a file")] 40 | NotAFile, 41 | #[error("unable to read image: {}", why)] 42 | ReadError { why: io::Error }, 43 | #[error("reached EOF prematurely")] 44 | Eof, 45 | } 46 | 47 | #[derive(Debug, Error)] 48 | #[rustfmt::skip] 49 | pub enum DiskError { 50 | #[error("failed to fetch devices from USB device stream: {}", _0)] 51 | DeviceStream(anyhow::Error), 52 | #[error("unable to open directory at '{}': {}", dir, why)] 53 | Directory { dir: &'static str, why: io::Error }, 54 | #[error("writing to the device was killed")] 55 | Killed, 56 | #[error("unable to read directory entry at '{}': invalid UTF-8", dir.display())] 57 | UTF8 { dir: Box }, 58 | #[error("unable to find disk '{}': {}", disk.display(), why)] 59 | NoDisk { disk: Box, why: io::Error }, 60 | #[error("failed to unmount {}: {}", path.display(), why)] 61 | UnmountCommand { path: Box, why: io::Error }, 62 | #[error("error using disk '{}': {} already mounted at {}", arg.display(), source_.display(), dest.display())] 63 | AlreadyMounted { arg: Box, source_: Box, dest: Box }, 64 | #[error("'{}' is not a block device", arg.display())] 65 | NotABlock { arg: Box }, 66 | #[error("unable to get metadata of disk '{}': {}", arg.display(), why)] 67 | Metadata { arg: Box, why: io::Error }, 68 | #[error("unable to open disk '{}': {}", disk.display(), why)] 69 | Open { disk: Box, why: io::Error }, 70 | #[error("error writing disk '{}': {}", disk.display(), why)] 71 | Write { disk: Box, why: io::Error }, 72 | #[error("error writing disk '{}': reached EOF", disk.display())] 73 | WriteEOF { disk: Box }, 74 | #[error("unable to flush disk '{}': {}", disk.display(), why)] 75 | Flush { disk: Box, why: io::Error }, 76 | #[error("error seeking disk '{}': seeked to {} instead of 0", disk.display(), invalid)] 77 | SeekInvalid { disk: Box, invalid: u64 }, 78 | #[error("error seeking disk '{}': {}", disk.display(), why)] 79 | Seek { disk: Box, why: io::Error }, 80 | #[error("error verifying disk '{}': {}", disk.display(), why)] 81 | Verify { disk: Box, why: io::Error }, 82 | #[error("error verifying disk '{}': reached EOF", disk.display())] 83 | VerifyEOF { disk: Box }, 84 | #[error("error verifying disk '{}': mismatch at {}:{}", disk.display(), x, y)] 85 | VerifyMismatch { disk: Box, x: usize, y: usize }, 86 | } 87 | 88 | pub async fn usb_disk_devices(disks: &mut Vec>) -> anyhow::Result<()> { 89 | let mut stream = UsbDiskProbe::new().await.context("failed to create USB disk probe")?; 90 | 91 | while let Some(device_result) = stream.next().await { 92 | match device_result { 93 | Ok(disk) => disks.push(PathBuf::from(&*disk).into_boxed_path()), 94 | Err(why) => { 95 | eprintln!("failed to reach device path: {}", why); 96 | } 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | 103 | /// Stores all discovered USB disk paths into the supplied `disks` vector. 104 | pub fn get_disk_args(disks: &mut Vec>) -> Result<(), DiskError> { 105 | executor::block_on( 106 | async move { usb_disk_devices(disks).await.map_err(DiskError::DeviceStream) }, 107 | ) 108 | } 109 | 110 | pub async fn disks_from_args>>( 111 | disk_args: D, 112 | mounts: &[MountEntry], 113 | unmount: bool, 114 | ) -> Result, File)>, DiskError> { 115 | let mut disks = Vec::new(); 116 | 117 | for disk_arg in disk_args { 118 | let canonical_path = fs::canonicalize(&disk_arg) 119 | .await 120 | .map_err(|why| DiskError::NoDisk { disk: disk_arg.clone(), why })?; 121 | 122 | for mount in mounts { 123 | if mount.spec.as_bytes().starts_with(canonical_path.as_os_str().as_bytes()) { 124 | if unmount { 125 | eprintln!( 126 | "unmounting '{}': {:?} is mounted at {:?}", 127 | disk_arg.display(), 128 | mount.spec, 129 | mount.file 130 | ); 131 | 132 | Command::new("umount").arg(&mount.spec).status().map_result().map_err( 133 | |why| DiskError::UnmountCommand { 134 | path: PathBuf::from(mount.spec.clone()).into_boxed_path(), 135 | why, 136 | }, 137 | )?; 138 | } else { 139 | return Err(DiskError::AlreadyMounted { 140 | arg: disk_arg.clone(), 141 | source_: PathBuf::from(mount.spec.clone()).into_boxed_path(), 142 | dest: PathBuf::from(mount.file.clone()).into_boxed_path(), 143 | }); 144 | } 145 | } 146 | } 147 | 148 | let metadata = canonical_path 149 | .metadata() 150 | .await 151 | .map_err(|why| DiskError::Metadata { arg: disk_arg.clone(), why })?; 152 | 153 | if !metadata.file_type().is_block_device() { 154 | return Err(DiskError::NotABlock { arg: disk_arg.clone() }); 155 | } 156 | 157 | let disk = OpenOptions::new() 158 | .read(true) 159 | .write(true) 160 | .custom_flags(libc::O_SYNC) 161 | .open(&canonical_path) 162 | .await 163 | .map_err(|why| DiskError::Open { disk: disk_arg.clone(), why })?; 164 | 165 | disks.push((canonical_path.into_boxed_path(), disk)); 166 | } 167 | 168 | Ok(disks) 169 | } 170 | -------------------------------------------------------------------------------- /gtk/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod events; 2 | pub mod signals; 3 | pub mod state; 4 | pub mod views; 5 | pub mod widgets; 6 | 7 | use self::events::*; 8 | use self::state::*; 9 | use self::views::*; 10 | use self::widgets::*; 11 | 12 | use crate::fl; 13 | use gtk::{self, prelude::*}; 14 | use std::{fs::File, process, rc::Rc, sync::Arc}; 15 | 16 | const CSS: &str = include_str!("ui.css"); 17 | 18 | pub struct App { 19 | pub ui: Rc, 20 | pub state: Arc, 21 | } 22 | 23 | impl App { 24 | pub fn new(state: State) -> Self { 25 | if gtk::init().is_err() { 26 | eprintln!("failed to initialize GTK Application"); 27 | process::exit(1); 28 | } 29 | 30 | App { ui: Rc::new(GtkUi::new()), state: Arc::new(state) } 31 | } 32 | 33 | pub fn connect_events(self) -> Self { 34 | self.connect_back(); 35 | self.connect_next(); 36 | self.connect_ui_events(); 37 | self.connect_image_chooser(); 38 | self.connect_image_drag_and_drop(); 39 | self.connect_hash(); 40 | self.connect_view_ready(); 41 | 42 | self 43 | } 44 | 45 | pub fn then_execute(self) { 46 | self.ui.window.show_all(); 47 | gtk::main(); 48 | } 49 | } 50 | 51 | pub struct GtkUi { 52 | window: gtk::Window, 53 | header: Header, 54 | content: Content, 55 | } 56 | 57 | impl GtkUi { 58 | pub fn new() -> Self { 59 | // Create a the headerbar and it's associated content. 60 | let header = Header::new(); 61 | let content = Content::new(); 62 | 63 | // Create a new top level window. 64 | let window = cascade! { 65 | gtk::Window::new(gtk::WindowType::Toplevel); 66 | // Set the headerbar as the title bar widget. 67 | ..set_titlebar(Some(&header.container)); 68 | // Set the title of the window. 69 | ..set_title("Popsicle"); 70 | // The default size of the window to create. 71 | ..set_default_size(500, 250); 72 | ..add(&content.container); 73 | }; 74 | 75 | // Add a custom CSS style 76 | let screen = WidgetExt::screen(&window).unwrap(); 77 | let style = gtk::CssProvider::new(); 78 | let _ = style.load_from_data(CSS.as_bytes()); 79 | gtk::StyleContext::add_provider_for_screen( 80 | &screen, 81 | &style, 82 | gtk::STYLE_PROVIDER_PRIORITY_USER, 83 | ); 84 | 85 | // The icon the app will display. 86 | gtk::Window::set_default_icon_name("com.system76.Popsicle"); 87 | 88 | // Programs what to do when the exit button is used. 89 | window.connect_delete_event(move |_, _| { 90 | gtk::main_quit(); 91 | gtk::Inhibit(false) 92 | }); 93 | 94 | GtkUi { header, window, content } 95 | } 96 | 97 | pub fn errorck( 98 | &self, 99 | state: &State, 100 | result: Result, 101 | context: &str, 102 | ) -> Result { 103 | result.map_err(|why| { 104 | self.content.error_view.view.description.set_text(&format!("{}: {}", context, why)); 105 | self.switch_to(state, ActiveView::Error); 106 | }) 107 | } 108 | 109 | pub fn errorck_option( 110 | &self, 111 | state: &State, 112 | result: Option, 113 | context: &'static str, 114 | ) -> Result { 115 | result.ok_or_else(|| { 116 | self.content.error_view.view.description.set_text(&format!( 117 | "{}: {}", 118 | context, 119 | fl!("no-value-found") 120 | )); 121 | self.switch_to(state, ActiveView::Error); 122 | }) 123 | } 124 | 125 | pub fn switch_to(&self, state: &State, view: ActiveView) { 126 | let back = &self.header.back; 127 | let next = &self.header.next; 128 | let stack = &self.content.container; 129 | 130 | let back_ctx = back.style_context(); 131 | let next_ctx = next.style_context(); 132 | 133 | let widget = match view { 134 | ActiveView::Images => { 135 | back.set_label(&fl!("cancel")); 136 | back_ctx.remove_class("back-button"); 137 | back_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 138 | 139 | next.set_label(&fl!("next")); 140 | next.set_visible(true); 141 | next.set_sensitive(true); 142 | next_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 143 | next_ctx.add_class(gtk::STYLE_CLASS_SUGGESTED_ACTION); 144 | 145 | &self.content.image_view.view.container 146 | } 147 | ActiveView::Devices => { 148 | next_ctx.remove_class(gtk::STYLE_CLASS_SUGGESTED_ACTION); 149 | next_ctx.add_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 150 | next.set_sensitive(false); 151 | 152 | let _ = state.back_event_tx.send(BackgroundEvent::RefreshDevices); 153 | &self.content.devices_view.view.container 154 | } 155 | ActiveView::Flashing => { 156 | match self.errorck( 157 | state, 158 | File::open(&*state.image_path.borrow()), 159 | &fl!("iso-open-failed"), 160 | ) { 161 | Ok(file) => *state.image.borrow_mut() = Some(file), 162 | Err(()) => return, 163 | }; 164 | 165 | let all_devices = state.available_devices.borrow(); 166 | let mut devices = state.selected_devices.borrow_mut(); 167 | 168 | devices.clear(); 169 | 170 | for active_id in self.content.devices_view.is_active_ids() { 171 | devices.push(all_devices[active_id].clone()); 172 | } 173 | 174 | back_ctx.remove_class("back-button"); 175 | back_ctx.add_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 176 | 177 | next.set_visible(false); 178 | &self.content.flash_view.view.container 179 | } 180 | ActiveView::Summary => { 181 | back_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 182 | back.set_label(&fl!("flash-again")); 183 | 184 | next_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 185 | next.set_visible(true); 186 | next.set_label(&fl!("done")); 187 | &self.content.summary_view.view.container 188 | } 189 | ActiveView::Error => { 190 | back.set_label(&fl!("flash-again")); 191 | back_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 192 | 193 | next.set_visible(true); 194 | next.set_label(&fl!("close")); 195 | next_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION); 196 | next_ctx.remove_class(gtk::STYLE_CLASS_SUGGESTED_ACTION); 197 | 198 | &self.content.error_view.view.container 199 | } 200 | }; 201 | 202 | stack.set_visible_child(widget); 203 | state.active_view.set(view); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /cli/src/main.rs: -------------------------------------------------------------------------------- 1 | //! CLI application for flashing multiple drives concurrently. 2 | 3 | #[macro_use] 4 | extern crate anyhow; 5 | #[macro_use] 6 | extern crate cascade; 7 | #[macro_use] 8 | extern crate derive_new; 9 | #[macro_use] 10 | extern crate fomat_macros; 11 | 12 | mod localize; 13 | 14 | use anyhow::Context; 15 | use async_std::{ 16 | fs::OpenOptions, 17 | os::unix::fs::OpenOptionsExt, 18 | path::{Path, PathBuf}, 19 | }; 20 | 21 | use clap::{builder::Arg, ArgAction, ArgMatches, Command}; 22 | use futures::{ 23 | channel::{mpsc, oneshot}, 24 | executor, join, 25 | prelude::*, 26 | }; 27 | use i18n_embed::DesktopLanguageRequester; 28 | use once_cell::sync::Lazy; 29 | use pbr::{MultiBar, Pipe, ProgressBar, Units}; 30 | use popsicle::{mnt, Progress, Task}; 31 | use std::{ 32 | io::{self, Write}, 33 | process, thread, 34 | }; 35 | 36 | static ARG_IMAGE: Lazy = Lazy::new(|| fl!("arg-image")); 37 | static ARG_DISKS: Lazy = Lazy::new(|| fl!("arg-disks")); 38 | 39 | fn main() { 40 | translate(); 41 | better_panic::install(); 42 | 43 | let matches = Command::new(env!("CARGO_PKG_NAME")) 44 | .about(env!("CARGO_PKG_DESCRIPTION")) 45 | .version(env!("CARGO_PKG_VERSION")) 46 | .arg(Arg::new(&**ARG_IMAGE).help(&fl!("arg-image-desc")).required(true)) 47 | .arg(Arg::new(&**ARG_DISKS).help(&fl!("arg-disks-desc"))) 48 | .arg( 49 | Arg::new("all") 50 | .help(&fl!("arg-all-desc")) 51 | .short('a') 52 | .long("all") 53 | .action(ArgAction::SetTrue), 54 | ) 55 | .arg( 56 | Arg::new("check") 57 | .help(&fl!("arg-check-desc")) 58 | .short('c') 59 | .long("check") 60 | .action(ArgAction::SetTrue), 61 | ) 62 | .arg( 63 | Arg::new("unmount") 64 | .help(&fl!("arg-unmount-desc")) 65 | .short('u') 66 | .long("unmount") 67 | .action(ArgAction::SetTrue), 68 | ) 69 | .arg( 70 | Arg::new("yes") 71 | .help(&fl!("arg-yes-desc")) 72 | .short('y') 73 | .long("yes") 74 | .action(ArgAction::SetTrue), 75 | ) 76 | .get_matches(); 77 | 78 | let (rtx, rrx) = oneshot::channel::>(); 79 | 80 | let result = executor::block_on(async move { 81 | match popsicle(rtx, matches).await { 82 | Err(why) => Err(why), 83 | _ => match rrx.await { 84 | Ok(Err(why)) => Err(why), 85 | _ => Ok(()), 86 | }, 87 | } 88 | }); 89 | 90 | if let Err(why) = result { 91 | eprintln!("popsicle: {}", why); 92 | for source in why.chain().skip(1) { 93 | epintln!(" " (fl!("error-caused-by")) ": " (source)) 94 | } 95 | 96 | process::exit(1); 97 | } 98 | } 99 | 100 | async fn popsicle( 101 | rtx: oneshot::Sender>, 102 | matches: ArgMatches, 103 | ) -> anyhow::Result<()> { 104 | let image_path = matches 105 | .get_one::(&fl!("arg-image")) 106 | .with_context(|| fl!("error-image-not-set"))? 107 | .clone(); 108 | 109 | let image = OpenOptions::new() 110 | .custom_flags(libc::O_SYNC) 111 | .read(true) 112 | .open(&image_path) 113 | .await 114 | .with_context(|| fl!("error-image-open", image_path = image_path.clone()))?; 115 | 116 | let image_size = image 117 | .metadata() 118 | .await 119 | .map(|x| x.len()) 120 | .with_context(|| fl!("error-image-metadata", image_path = image_path.clone()))?; 121 | 122 | let mut disk_args = Vec::new(); 123 | if matches.get_flag("all") { 124 | popsicle::usb_disk_devices(&mut disk_args) 125 | .await 126 | .with_context(|| fl!("error-disks-fetch"))?; 127 | } else if let Some(disks) = matches.get_many::(&fl!("arg-disks")) { 128 | disk_args.extend(disks.map(PathBuf::from).map(Box::from)); 129 | } 130 | 131 | if disk_args.is_empty() { 132 | return Err(anyhow!(fl!("error-no-disks-specified"))); 133 | } 134 | 135 | let mounts = mnt::get_submounts(Path::new("/")).with_context(|| fl!("error-reading-mounts"))?; 136 | 137 | let disks = 138 | popsicle::disks_from_args(disk_args.into_iter(), &mounts, matches.get_flag("unmount")) 139 | .await 140 | .with_context(|| fl!("error-opening-disks"))?; 141 | 142 | let is_tty = atty::is(atty::Stream::Stdout); 143 | 144 | if is_tty && !matches.get_flag("yes") { 145 | epint!( 146 | (fl!("question", image_path = image_path)) "\n" 147 | for (path, _) in &disks { 148 | " - " (path.display()) "\n" 149 | } 150 | (fl!("yn")) ": " 151 | ); 152 | 153 | io::stdout().flush().unwrap(); 154 | 155 | let mut confirm = String::new(); 156 | io::stdin().read_line(&mut confirm).unwrap(); 157 | 158 | if confirm.trim() != fl!("y") && confirm.trim() != "yes" { 159 | return Err(anyhow!(fl!("error-exiting"))); 160 | } 161 | } 162 | 163 | let check = matches.get_flag("check"); 164 | 165 | // If this is a TTY, display a progress bar. If not, display machine-readable info. 166 | if is_tty { 167 | println!(); 168 | 169 | let mb = MultiBar::new(); 170 | let mut task = Task::new(image, check); 171 | 172 | for (disk_path, disk) in disks { 173 | let pb = InteractiveProgress::new(cascade! { 174 | mb.create_bar(image_size); 175 | ..set_units(Units::Bytes); 176 | ..message(&format!("W {}: ", disk_path.display())); 177 | }); 178 | 179 | task.subscribe(disk, disk_path, pb); 180 | } 181 | 182 | thread::spawn(|| { 183 | executor::block_on(async move { 184 | let buf = &mut [0u8; 64 * 1024]; 185 | let _ = rtx.send(task.process(buf).await); 186 | }) 187 | }); 188 | 189 | mb.listen(); 190 | } else { 191 | let (etx, erx) = mpsc::unbounded(); 192 | let mut paths = Vec::new(); 193 | let mut task = Task::new(image, check); 194 | 195 | for (disk_path, disk) in disks { 196 | let pb = MachineProgress::new(paths.len(), etx.clone()); 197 | paths.push(disk_path.clone()); 198 | task.subscribe(disk, disk_path, pb); 199 | } 200 | 201 | drop(etx); 202 | 203 | let task = async move { 204 | let buf = &mut [0u8; 64 * 1024]; 205 | let _ = rtx.send(task.process(buf).await); 206 | }; 207 | 208 | join!(machine_output(erx, &paths, image_size), task); 209 | } 210 | 211 | Ok(()) 212 | } 213 | 214 | /// An event for creating a machine-readable output 215 | pub enum Event { 216 | Message(usize, Box), 217 | Finished(usize), 218 | Set(usize, u64), 219 | } 220 | 221 | /// Tracks progress 222 | #[derive(new)] 223 | pub struct MachineProgress { 224 | id: usize, 225 | 226 | handle: mpsc::UnboundedSender, 227 | } 228 | 229 | impl Progress for MachineProgress { 230 | type Device = Box; 231 | 232 | fn message(&mut self, _path: &Box, kind: &str, message: &str) { 233 | let _ = self.handle.unbounded_send(Event::Message( 234 | self.id, 235 | if message.is_empty() { kind.into() } else { [kind, " ", message].concat().into() }, 236 | )); 237 | } 238 | 239 | fn finish(&mut self) { 240 | let _ = self.handle.unbounded_send(Event::Finished(self.id)); 241 | } 242 | 243 | fn set(&mut self, written: u64) { 244 | let _ = self.handle.unbounded_send(Event::Set(self.id, written)); 245 | } 246 | } 247 | 248 | #[derive(new)] 249 | pub struct InteractiveProgress { 250 | pipe: ProgressBar, 251 | } 252 | 253 | impl Progress for InteractiveProgress { 254 | type Device = Box; 255 | 256 | fn message(&mut self, path: &Box, kind: &str, message: &str) { 257 | self.pipe.message(&format!("{} {}: {}", kind, path.display(), message)); 258 | } 259 | 260 | fn finish(&mut self) { 261 | self.pipe.finish(); 262 | } 263 | 264 | fn set(&mut self, written: u64) { 265 | self.pipe.set(written); 266 | } 267 | } 268 | 269 | /// Writes a machine-friendly output, when this program is being piped into another. 270 | async fn machine_output( 271 | mut rx: mpsc::UnboundedReceiver, 272 | paths: &[Box], 273 | image_size: u64, 274 | ) { 275 | let stdout = io::stdout(); 276 | let stdout = &mut stdout.lock(); 277 | 278 | let _ = wite!( 279 | stdout, 280 | "Size(" (image_size) ")\n" 281 | for path in paths { 282 | "Device(\"" (path.display()) "\")\n" 283 | } 284 | ); 285 | 286 | while let Some(event) = rx.next().await { 287 | match event { 288 | Event::Message(id, message) => { 289 | let _ = witeln!(stdout, "Message(\"" (paths[id].display()) "\",\"" (message) "\")"); 290 | } 291 | Event::Finished(id) => { 292 | let _ = witeln!(stdout, "Finished(\"" (paths[id].display()) "\")"); 293 | } 294 | Event::Set(id, written) => { 295 | let _ = witeln!(stdout, "Set(\"" (paths[id].display()) "\"," (written) ")"); 296 | } 297 | } 298 | } 299 | } 300 | 301 | fn translate() { 302 | let requested_languages = DesktopLanguageRequester::requested_languages(); 303 | let localizer = crate::localize::localizer(); 304 | 305 | if let Err(error) = localizer.select(&requested_languages) { 306 | eprintln!("Error while loading languages for popsicle-cli {}", error); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /gtk/src/app/signals/mod.rs: -------------------------------------------------------------------------------- 1 | mod devices; 2 | mod images; 3 | 4 | use crate::app::events::{BackgroundEvent, UiEvent}; 5 | use crate::app::state::ActiveView; 6 | use crate::app::App; 7 | use crate::fl; 8 | use crate::flash::{FlashRequest, FlashStatus, FlashTask}; 9 | use crate::misc; 10 | use atomic::Atomic; 11 | use crossbeam_channel::TryRecvError; 12 | use gtk::{self, prelude::*}; 13 | use iso9660::ISO9660; 14 | use std::fmt::Write; 15 | use std::fs::File; 16 | use std::sync::atomic::Ordering; 17 | use std::sync::{Arc, Mutex}; 18 | use std::time::{Duration, Instant}; 19 | 20 | impl App { 21 | pub fn connect_back(&self) { 22 | let state = self.state.clone(); 23 | let ui = self.ui.clone(); 24 | 25 | self.ui.header.connect_back(move || { 26 | let back = match state.active_view.get() { 27 | ActiveView::Images => { 28 | gtk::main_quit(); 29 | return; 30 | } 31 | _ => ActiveView::Images, 32 | }; 33 | 34 | let _ = state.ui_event_tx.send(UiEvent::Reset); 35 | ui.content.devices_view.reset(); 36 | 37 | ui.switch_to(&state, back); 38 | }); 39 | } 40 | 41 | pub fn connect_next(&self) { 42 | let state = self.state.clone(); 43 | let ui = self.ui.clone(); 44 | 45 | self.ui.header.connect_next(move || { 46 | let next = match state.active_view.get() { 47 | ActiveView::Images => ActiveView::Devices, 48 | ActiveView::Devices => ActiveView::Flashing, 49 | _ => { 50 | gtk::main_quit(); 51 | return; 52 | } 53 | }; 54 | 55 | ui.switch_to(&state, next); 56 | }); 57 | } 58 | 59 | pub fn connect_ui_events(&self) { 60 | let state = self.state.clone(); 61 | let ui = self.ui.clone(); 62 | 63 | let mut last_device_refresh = Instant::now(); 64 | let mut flashing_devices: Vec<(gtk::ProgressBar, gtk::Label)> = Vec::new(); 65 | let flash_status = Arc::new(Atomic::new(FlashStatus::Inactive)); 66 | let mut flash_handles = None; 67 | let mut tasks = None; 68 | 69 | glib::timeout_add_local(Duration::from_millis(16), move || { 70 | match state.ui_event_rx.try_recv() { 71 | Err(TryRecvError::Disconnected) => return Continue(false), 72 | Err(TryRecvError::Empty) => (), 73 | Ok(UiEvent::SetHash(hash)) => { 74 | ui.content.image_view.set_hash(&match hash { 75 | Ok(hash) => hash, 76 | Err(why) => fl!("error", why = format!("{}", why)), 77 | }); 78 | ui.content.image_view.set_hash_sensitive(true); 79 | 80 | ui.content.image_view.chooser_container.set_visible_child_name("chooser"); 81 | } 82 | Ok(UiEvent::SetImageLabel(path)) => { 83 | if let Ok(file) = File::open(&path) { 84 | let image_size = file.metadata().ok().map_or(0, |m| m.len()); 85 | 86 | let warning = if is_windows_iso(&file) { 87 | Some(fl!("win-isos-not-supported")) 88 | } else { 89 | None 90 | }; 91 | 92 | ui.content.image_view.set_image(&path, image_size, warning.as_deref()); 93 | ui.content.image_view.set_hash_sensitive(true); 94 | ui.header.next.set_sensitive(true); 95 | 96 | state.image_size.store(image_size, Ordering::SeqCst); 97 | *state.image_path.borrow_mut() = path; 98 | } 99 | } 100 | Ok(UiEvent::RefreshDevices(devices)) => { 101 | let size = state.image_size.load(Ordering::SeqCst); 102 | ui.content.devices_view.refresh(&devices, size); 103 | *state.available_devices.borrow_mut() = devices; 104 | } 105 | Ok(UiEvent::Flash(handle)) => flash_handles = Some(handle), 106 | Ok(UiEvent::Reset) => { 107 | match flash_status.load(Ordering::SeqCst) { 108 | FlashStatus::Active => { 109 | flash_status.store(FlashStatus::Killing, Ordering::SeqCst) 110 | } 111 | FlashStatus::Inactive | FlashStatus::Killing => (), 112 | } 113 | 114 | flash_handles = None; 115 | tasks = None; 116 | flashing_devices.clear(); 117 | } 118 | } 119 | 120 | match state.active_view.get() { 121 | ActiveView::Devices => { 122 | let now = Instant::now(); 123 | 124 | // Only attempt to refresh the devices if the last refresh was >= 3 seconds ago. 125 | if now.duration_since(last_device_refresh).as_secs() >= 3 { 126 | last_device_refresh = now; 127 | let _ = state.back_event_tx.send(BackgroundEvent::RefreshDevices); 128 | } 129 | } 130 | ActiveView::Flashing => match state.image.borrow_mut().take() { 131 | // When the flashing view is active, and an image has not started flashing. 132 | Some(image) => { 133 | let summary_grid = &ui.content.flash_view.progress_list; 134 | summary_grid.foreach(|w| summary_grid.remove(w)); 135 | let mut destinations = Vec::new(); 136 | 137 | let selected_devices = state.selected_devices.borrow_mut(); 138 | for (id, device) in selected_devices.iter().enumerate() { 139 | let id = id as i32; 140 | 141 | let pbar = cascade! { 142 | gtk::ProgressBar::new(); 143 | ..set_hexpand(true); 144 | }; 145 | 146 | let label = cascade! { 147 | gtk::Label::new(Some(&misc::device_label(device))); 148 | ..set_justify(gtk::Justification::Right); 149 | ..style_context().add_class("bold"); 150 | }; 151 | 152 | let bar_label = cascade! { 153 | gtk::Label::new(None); 154 | ..set_halign(gtk::Align::Center); 155 | }; 156 | 157 | let bar_container = cascade! { 158 | gtk::Box::new(gtk::Orientation::Vertical, 0); 159 | ..add(&pbar); 160 | ..add(&bar_label); 161 | }; 162 | 163 | summary_grid.attach(&label, 0, id, 1, 1); 164 | summary_grid.attach(&bar_container, 1, id, 1, 1); 165 | 166 | flashing_devices.push((pbar, bar_label)); 167 | destinations.push(device.clone()); 168 | } 169 | 170 | summary_grid.show_all(); 171 | let ndestinations = destinations.len(); 172 | let progress = Arc::new( 173 | (0..ndestinations).map(|_| Atomic::new(0u64)).collect::>(), 174 | ); 175 | let finished = Arc::new( 176 | (0..ndestinations).map(|_| Atomic::new(false)).collect::>(), 177 | ); 178 | 179 | let _ = 180 | state.back_event_tx.send(BackgroundEvent::Flash(FlashRequest::new( 181 | image, 182 | destinations, 183 | flash_status.clone(), 184 | progress.clone(), 185 | finished.clone(), 186 | ))); 187 | 188 | tasks = Some(FlashTask { 189 | previous: Arc::new(Mutex::new(vec![[0; 7]; ndestinations])), 190 | progress, 191 | finished, 192 | }); 193 | } 194 | // When the flashing view is active, and thus an image is flashing. 195 | None => { 196 | let now = Instant::now(); 197 | 198 | // Only attempt to refresh the devices if the last refresh was >= 500ms ago. 199 | let time_since = now.duration_since(last_device_refresh); 200 | if time_since.as_secs() > 1 || time_since.subsec_millis() >= 500 { 201 | last_device_refresh = now; 202 | 203 | let mut all_tasks_finished = true; 204 | let length = state.image_size.load(Ordering::SeqCst); 205 | let tasks = tasks.as_mut().expect("no flash task"); 206 | let mut previous = tasks.previous.lock().expect("mutex lock"); 207 | 208 | for (id, (pbar, label)) in flashing_devices.iter().enumerate() { 209 | let prev_values = &mut previous[id]; 210 | let progress = &tasks.progress[id]; 211 | let finished = &tasks.finished[id]; 212 | 213 | let raw_value = progress.load(Ordering::SeqCst); 214 | let task_is_finished = finished.load(Ordering::SeqCst); 215 | let value = if task_is_finished { 216 | 1.0f64 217 | } else { 218 | all_tasks_finished = false; 219 | raw_value as f64 / length as f64 220 | }; 221 | 222 | pbar.set_fraction(value); 223 | 224 | if task_is_finished { 225 | label.set_label(&fl!("task-finished")); 226 | } else { 227 | prev_values[1] = prev_values[2]; 228 | prev_values[2] = prev_values[3]; 229 | prev_values[3] = prev_values[4]; 230 | prev_values[4] = prev_values[5]; 231 | prev_values[5] = prev_values[6]; 232 | prev_values[6] = raw_value - prev_values[0]; 233 | prev_values[0] = raw_value; 234 | 235 | let sum: u64 = prev_values.iter().skip(1).sum(); 236 | let per_second = sum / 3; 237 | label.set_label(&format!( 238 | "{}/s", 239 | bytesize::to_string(per_second, true) 240 | )); 241 | } 242 | } 243 | 244 | drop(previous); 245 | 246 | if all_tasks_finished { 247 | eprintln!("all tasks finished"); 248 | 249 | let taken_handles = match ui.errorck_option( 250 | &state, 251 | flash_handles.take(), 252 | "Taking flash handles failed", 253 | ) { 254 | Ok(results) => { 255 | results.join().map_err(|why| format!("{:?}", why)) 256 | } 257 | Err(()) => return Continue(true), 258 | }; 259 | 260 | let handle = match ui.errorck( 261 | &state, 262 | taken_handles, 263 | "Failed to join flash thread", 264 | ) { 265 | Ok(results) => results, 266 | Err(()) => return Continue(true), 267 | }; 268 | 269 | let (result, results) = match ui.errorck( 270 | &state, 271 | handle, 272 | "Errored starting flashing process", 273 | ) { 274 | Ok(result) => result, 275 | Err(()) => return Continue(true), 276 | }; 277 | 278 | let mut errors = Vec::new(); 279 | let mut selected_devices = state.selected_devices.borrow_mut(); 280 | let ntasks = selected_devices.len(); 281 | 282 | for (device, result) in 283 | selected_devices.drain(..).zip(results.into_iter()) 284 | { 285 | if let Err(why) = result { 286 | errors.push((device, why)); 287 | } 288 | } 289 | 290 | ui.switch_to(&state, ActiveView::Summary); 291 | let list = &ui.content.summary_view.list; 292 | let description = &ui.content.summary_view.view.description; 293 | 294 | if result.is_ok() && errors.is_empty() { 295 | let desc = fl!("successful-flash", total = ntasks); 296 | description.set_text(&desc); 297 | list.hide(); 298 | } else { 299 | ui.content 300 | .summary_view 301 | .view 302 | .topic 303 | .set_text(&fl!("flashing-completed-with-errors")); 304 | 305 | let mut desc = fl!( 306 | "partial-flash", 307 | number = { ntasks - errors.len() }, 308 | total = ntasks 309 | ); 310 | 311 | if let Err(why) = result { 312 | let _ = write!(desc, ": {}", why); 313 | } 314 | 315 | description.set_markup(&desc); 316 | 317 | for (device, why) in errors { 318 | let device = 319 | gtk::Label::new(Some(&misc::device_label(&device))); 320 | let why = 321 | gtk::Label::new(Some(format!("{}", why).as_str())); 322 | why.style_context().add_class("bold"); 323 | 324 | let container = cascade! { 325 | gtk::Box::new(gtk::Orientation::Horizontal, 6); 326 | ..pack_start(&device, false, false, 0); 327 | ..pack_start(&why, true, true, 0); 328 | }; 329 | 330 | let row = cascade! { 331 | gtk::ListBoxRow::new(); 332 | ..set_selectable(false); 333 | ..add(&container); 334 | }; 335 | 336 | list.add(&row); 337 | } 338 | 339 | list.show_all(); 340 | } 341 | } 342 | } 343 | } 344 | }, 345 | _ => (), 346 | } 347 | 348 | Continue(true) 349 | }); 350 | } 351 | } 352 | 353 | fn is_windows_iso(file: &File) -> bool { 354 | if let Ok(fs) = ISO9660::new(file) { 355 | return fs.publisher_identifier() == "MICROSOFT CORPORATION"; 356 | } 357 | false 358 | } 359 | --------------------------------------------------------------------------------