Skip to content

Commit

Permalink
Added hooks for windows default file handling
Browse files Browse the repository at this point in the history
  • Loading branch information
dividebysandwich committed Feb 26, 2024
1 parent b163162 commit 251bdd4
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 18 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"rust-analyzer.linkedProjects": [
".\\Cargo.toml"
]
],
"rust-analyzer.showUnlinkedFileNotification": false
}
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ edition = "2021"
build = "build.rs"

[dependencies]
const_format = "0.2.32"
fltk ={ version = "1.4.25", features = ["fltk-bundled"] }
imagepipe = "0.5.0"
windows = { version = "0.53.0", features = ["Win32_UI_Shell"]}
winreg = "0.52.0"


[build-dependencies]
winres = "0.1.12"
windows = "0.53.0"

10 changes: 5 additions & 5 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
extern crate winres;

fn main() {
if cfg!(target_os = "windows") {
let mut res = winres::WindowsResource::new();
res.set_icon("lightningview.ico"); // Replace this with the filename of your .ico file.
res.compile().unwrap();
}
if cfg!(target_os = "windows") {
let mut res = winres::WindowsResource::new();
res.set_icon("lightningview.ico"); // Replace this with the filename of your .ico file.
res.compile().unwrap();
}
}
56 changes: 44 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
//#[cfg(target_os = "windows")]
//#![windows_subsystem = "windows"]

use fltk::{app::{self, MouseWheel}, enums::Color, frame::Frame, image::SharedImage, prelude::*, window::Window};
use std::{env, error::Error, fs, path::{Path, PathBuf}};

fn load_and_display_image(original_image: &mut SharedImage, frame: &mut Frame, wind: &mut Window, path: &PathBuf, fltk_supported_formats: Vec<&str>, raw_supported_formats: Vec<&str>) {
if let Ok(image) = load_image(&path.to_string_lossy(), fltk_supported_formats, raw_supported_formats) {
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
use crate::windows::*;
#[cfg(target_os = "unix")]
mod notwindows;
#[cfg(target_os = "unix")]
use crate::notwindows::*;
#[cfg(target_os = "macos")]
mod notwindows;
#[cfg(target_os = "macos")]
use crate::notwindows::*;

pub const FLTK_SUPPORTED_FORMATS: [&str; 6] = ["jpg", "jpeg", "png", "bmp", "gif", "svg"];
pub const RAW_SUPPORTED_FORMATS: [&str; 23] = ["mrw", "arw", "srf", "sr2", "nef", "mef", "orf", "srw", "erf", "kdc", "dcs", "rw2", "raf", "dcr", "dng", "pef", "crw", "iiq", "3fr", "nrw", "mos", "cr2", "ari"];


fn load_and_display_image(original_image: &mut SharedImage, frame: &mut Frame, wind: &mut Window, path: &PathBuf) {
if let Ok(image) = load_image(&path.to_string_lossy()) {
let mut new_image = image.clone();
new_image.scale(wind.width(), wind.height(), true, true);
frame.set_image(Some(new_image));
Expand Down Expand Up @@ -51,13 +71,13 @@ fn load_raw(image_file: &str) -> Result<SharedImage, String> {
}
}

fn load_image(image_file: &str, fltk_supported_formats: Vec<&str>, raw_supported_formats: Vec<&str>) -> Result<SharedImage, String> {
if fltk_supported_formats.iter().any(|&format| image_file.to_lowercase().ends_with(format)) {
fn load_image(image_file: &str) -> Result<SharedImage, String> {
if FLTK_SUPPORTED_FORMATS.iter().any(|&format| image_file.to_lowercase().ends_with(format)) {
match SharedImage::load(image_file) {
Ok(image) => Ok(image),
Err(err) => Err(format!("Error loading image: {}", err)),
}
} else if raw_supported_formats.iter().any(|&format| image_file.to_lowercase().ends_with(format)) {
} else if RAW_SUPPORTED_FORMATS.iter().any(|&format| image_file.to_lowercase().ends_with(format)) {
match load_raw(image_file) {
Ok(image) => Ok(image),
Err(err) => Err(format!("Error loading image: {}", err)),
Expand All @@ -69,18 +89,30 @@ fn load_image(image_file: &str, fltk_supported_formats: Vec<&str>, raw_supported
}

fn main() -> Result<(), Box<dyn Error>> {
let fltk_supported_formats = ["jpg", "jpeg", "png", "bmp", "gif", "svg"];
let raw_supported_formats = ["mrw", "arw", "srf", "sr2", "nef", "mef", "orf", "srw", "erf", "kdc", "dcs", "rw2", "raf", "dcr", "dng", "pef", "crw", "iiq", "3fr", "nrw", "mos", "cr2", "ari"];

let args: Vec<String> = env::args().collect();

if args.len() < 2 {
println!("Usage: {} <image_file>", args[0]);
println!("To register as image viewer in Windows, run: {} /register", args[0]);
println!("To unregister, run: {} /unregister", args[0]);
std::process::exit(1);
}

let image_file = &args[1];

if image_file.eq_ignore_ascii_case("/register") {
match register_urlhandler() {
Ok(_) => println!("Success! LightningView egistered as image viewer."),
Err(err) => println!("Failed to register as image viewer: {}", err),
}
std::process::exit(0);
} else if image_file.eq_ignore_ascii_case("/unregister") {
unregister_urlhandler();
println!("LightningView unregistered as image viewer.");
std::process::exit(0);
}

let app = app::App::default();

// Get the screen size
Expand All @@ -97,7 +129,7 @@ fn main() -> Result<(), Box<dyn Error>> {
wind.fullscreen(true);
let mut frame = Frame::default_fill();

let mut original_image = load_image(image_file, fltk_supported_formats.to_vec(), raw_supported_formats.to_vec())?;
let mut original_image = load_image(image_file)?;
let mut image = original_image.clone();
image.scale(wind.width(), wind.height(), true, true);

Expand Down Expand Up @@ -125,8 +157,8 @@ fn main() -> Result<(), Box<dyn Error>> {

if let Ok(entries) = fs::read_dir(parent_dir) {
let mut all_supported_formats: Vec<&str> = Vec::new();
all_supported_formats.extend(&fltk_supported_formats);
all_supported_formats.extend(&raw_supported_formats);
all_supported_formats.extend(&FLTK_SUPPORTED_FORMATS);
all_supported_formats.extend(&RAW_SUPPORTED_FORMATS);
image_files = entries
.filter_map(|entry| entry.ok().map(|e| e.path()))
.filter(|path| {
Expand Down Expand Up @@ -210,7 +242,7 @@ fn main() -> Result<(), Box<dyn Error>> {
if !image_files.is_empty() {
current_index = (current_index + image_files.len() - 1) % image_files.len();
println!("Loading previous image: {}", image_files[current_index].display());
load_and_display_image(&mut original_image, &mut frame, &mut wind, &image_files[current_index], fltk_supported_formats.to_vec(), raw_supported_formats.to_vec());
load_and_display_image(&mut original_image, &mut frame, &mut wind, &image_files[current_index]);
zoom_factor = 1.0;
frame.set_pos(0, 0);
wind.redraw();
Expand All @@ -220,7 +252,7 @@ fn main() -> Result<(), Box<dyn Error>> {
if !image_files.is_empty() {
current_index = (current_index + 1) % image_files.len();
println!("Loading next image: {}", image_files[current_index].display());
load_and_display_image(&mut original_image, &mut frame, &mut wind, &image_files[current_index], fltk_supported_formats.to_vec(), raw_supported_formats.to_vec());
load_and_display_image(&mut original_image, &mut frame, &mut wind, &image_files[current_index]);
zoom_factor = 1.0;
frame.set_pos(0, 0);
wind.redraw();
Expand Down
8 changes: 8 additions & 0 deletions src/notwindows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use std::io;


pub fn register_urlhandler() -> io::Result<()> {
}

pub fn unregister_urlhandler() {
}
202 changes: 202 additions & 0 deletions src/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#![allow(dead_code)]
use const_format::concatcp;
use std::{
error::Error,
io,
path::PathBuf,
};
use winreg::{enums::*, RegKey};

use crate::{FLTK_SUPPORTED_FORMATS, RAW_SUPPORTED_FORMATS};
const CANONICAL_NAME: &str = "lightningview.exe";
const PROGID: &str = "LightningViewImageFile";

// Configuration for "Default Programs". StartMenuInternet is the key for browsers
// and they're expected to use the name of the exe as the key.
const DPROG_PATH: &str = concatcp!(r"SOFTWARE\Clients\StartMenuInternet\", CANONICAL_NAME);
const DPROG_INSTALLINFO_PATH: &str = concatcp!(DPROG_PATH, "InstallInfo");

const APPREG_BASE: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\";
const PROGID_PATH: &str = concatcp!(r"SOFTWARE\Classes\", PROGID);
const REGISTERED_APPLICATIONS_PATH: &str =
concatcp!(r"SOFTWARE\RegisteredApplications\", DISPLAY_NAME);

const DISPLAY_NAME: &str = "Lightning View Image Viewer";
const DESCRIPTION: &str = "Simple No-Fuss image viewer and browser";

/// Retrieve an EXE path by looking in the registry for the App Paths entry
fn get_exe_path(exe_name: &str) -> Result<PathBuf, Box<dyn Error>> {
for root_name in &[HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE] {
let root = RegKey::predef(*root_name);
if let Ok(subkey) = root.open_subkey(format!("{}{}", APPREG_BASE, exe_name)) {
if let Ok(value) = subkey.get_value::<String, _>("") {
let path = PathBuf::from(value);
if path.is_file() {
return Ok(path);
}
}
}
}

Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
format!("Could not find path for {}", exe_name),
)))
}

/// Register associations with Windows for being a browser
pub fn register_urlhandler() -> io::Result<()> {
// This is used both by initial registration and OS-invoked reinstallation.
// The expectations for the latter are documented here: https://docs.microsoft.com/en-us/windows/win32/shell/reg-middleware-apps#the-reinstall-command
use std::env::current_exe;

let exe_path = current_exe()?;
let exe_name = exe_path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_owned();

let exe_path = exe_path.to_str().unwrap_or_default().to_owned();
let icon_path = format!("\"{}\",0", exe_path);
let open_command = format!("\"{}\" \"%1\"", exe_path);

let hkcu = RegKey::predef(HKEY_CURRENT_USER);

// Configure our ProgID to point to the right command
{
let (progid_class, _) = hkcu.create_subkey(PROGID_PATH)?;
progid_class.set_value("", &DISPLAY_NAME)?;

let (progid_class_defaulticon, _) = progid_class.create_subkey("DefaultIcon")?;
progid_class_defaulticon.set_value("", &icon_path)?;

let (progid_class_shell_open_command, _) =
progid_class.create_subkey(r"shell\open\command")?;
progid_class_shell_open_command.set_value("", &open_command)?;
}

// Set up the Default Programs configuration for the app (https://docs.microsoft.com/en-us/windows/win32/shell/default-programs)
{
let (dprog, _) = hkcu.create_subkey(DPROG_PATH)?;
dprog.set_value("", &DISPLAY_NAME)?;
dprog.set_value("LocalizedString", &DISPLAY_NAME)?;

let (dprog_capabilites, _) = dprog.create_subkey("Capabilities")?;
dprog_capabilites.set_value("ApplicationName", &DISPLAY_NAME)?;
dprog_capabilites.set_value("ApplicationIcon", &icon_path)?;
dprog_capabilites.set_value("ApplicationDescription", &DESCRIPTION)?;

let (dprog_capabilities_startmenu, _) = dprog_capabilites.create_subkey("Startmenu")?;
dprog_capabilities_startmenu.set_value("StartMenuInternet", &CANONICAL_NAME)?;

// Register for various file types, so that we'll be invoked for file:// URLs for these types (e.g.
// by `cargo doc --open`.)
let (dprog_capabilities_fileassociations, _) =
dprog_capabilites.create_subkey("FileAssociations")?;

let mut all_supported_formats: Vec<&str> = Vec::new();
all_supported_formats.extend(&FLTK_SUPPORTED_FORMATS);
all_supported_formats.extend(&RAW_SUPPORTED_FORMATS);

for filetype in all_supported_formats {
dprog_capabilities_fileassociations.set_value(filetype, &PROGID)?;
}

let (dprog_defaulticon, _) = dprog.create_subkey("DefaultIcon")?;
dprog_defaulticon.set_value("", &icon_path)?;

// Set up reinstallation and show/hide icon commands (https://docs.microsoft.com/en-us/windows/win32/shell/reg-middleware-apps#registering-installation-information)
let (dprog_installinfo, _) = dprog.create_subkey("InstallInfo")?;
dprog_installinfo.set_value("ReinstallCommand", &format!("\"{}\" register", exe_path))?;
dprog_installinfo.set_value("HideIconsCommand", &format!("\"{}\" hide-icons", exe_path))?;
dprog_installinfo.set_value("ShowIconsCommand", &format!("\"{}\" show-icons", exe_path))?;

// Only update IconsVisible if it hasn't been set already
if dprog_installinfo
.get_value::<u32, _>("IconsVisible")
.is_err()
{
dprog_installinfo.set_value("IconsVisible", &1u32)?;
}

let (dprog_shell_open_command, _) = dprog.create_subkey(r"shell\open\command")?;
dprog_shell_open_command.set_value("", &open_command)?;
}

// Set up a registered application for our Default Programs capabilities (https://docs.microsoft.com/en-us/windows/win32/shell/default-programs#registeredapplications)
{
let (registered_applications, _) =
hkcu.create_subkey(r"SOFTWARE\RegisteredApplications")?;
let dprog_capabilities_path = format!(r"{}\Capabilities", DPROG_PATH);
registered_applications.set_value(DISPLAY_NAME, &dprog_capabilities_path)?;
}

// Application Registration (https://docs.microsoft.com/en-us/windows/win32/shell/app-registration)
{
let appreg_path = format!(r"{}{}", APPREG_BASE, exe_name);
let (appreg, _) = hkcu.create_subkey(appreg_path)?;
// This is used to resolve "lightningview.exe" -> full path, if needed.
appreg.set_value("", &exe_path)?;
}

refresh_shell();

Ok(())
}

fn refresh_shell() {
use windows::Win32::UI::Shell::{SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_DWORD, SHCNF_FLUSH};

// Notify the shell about the updated URL associations. (https://docs.microsoft.com/en-us/windows/win32/shell/default-programs#becoming-the-default-browser)
unsafe {
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_DWORD | SHCNF_FLUSH, None, None);
}
}

/// Remove all the registry keys that we've set up
pub fn unregister_urlhandler() {
use std::env::current_exe;

// Find the current executable's name, so we can unregister it
let exe_name = current_exe()
.unwrap()
.file_name()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_owned();

let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let _ = hkcu.delete_subkey_all(DPROG_PATH);
let _ = hkcu.delete_subkey_all(PROGID_PATH);
let _ = hkcu.delete_subkey(REGISTERED_APPLICATIONS_PATH);
let _ = hkcu.delete_subkey_all(format!("{}{}", APPREG_BASE, exe_name));
refresh_shell();
}

/// Set the "IconsVisible" flag to true (we don't have any icons)
fn show_icons() -> io::Result<()> {
// The expectations for this are documented here: https://docs.microsoft.com/en-us/windows/win32/shell/reg-middleware-apps#the-show-icons-command
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (dprog_installinfo, _) = hkcu.create_subkey(DPROG_INSTALLINFO_PATH)?;
dprog_installinfo.set_value("IconsVisible", &1u32)
}

/// Set the "IconsVisible" flag to false (we don't have any icons)
fn hide_icons() -> io::Result<()> {
// The expectations for this are documented here: https://docs.microsoft.com/en-us/windows/win32/shell/reg-middleware-apps#the-hide-icons-command
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
if let Ok(dprog_installinfo) = hkcu.open_subkey(DPROG_INSTALLINFO_PATH) {
dprog_installinfo.set_value("IconsVisible", &0u32)
} else {
Ok(())
}
}

fn get_exe_relative_path(filename: &str) -> io::Result<PathBuf> {
let mut path = std::env::current_exe()?;
path.set_file_name(filename);
Ok(path)
}


0 comments on commit 251bdd4

Please sign in to comment.