Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FileSelection Widget #748

Merged
merged 8 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions crates/livesplit-auto-splitting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,58 @@ extern "C" {
option_description_ptr: *const u8,
option_description_len: usize,
) -> bool;
/// Adds a new file select setting that the user can modify. This allows the
/// user to choose a file from the file system. The key is used to store the
/// path of the file in the settings map and needs to be unique across all
/// types of settings. The description is what's shown to the user. The
/// pointers need to point to valid UTF-8 encoded text with the respective
/// given length. The path is a path that is accessible through the WASI
/// file system, so a Windows path of `C:\foo\bar.exe` would be stored as
/// `/mnt/c/foo/bar.exe`.
pub fn user_settings_add_file_select(
key_ptr: *const u8,
key_len: usize,
description_ptr: *const u8,
description_len: usize,
);
/// Adds a filter to a file select setting. The key needs to match the key
/// of the file select setting that it's supposed to be added to. The
/// description is what's shown to the user for the specific filter. The
/// description is optional. You may provide a null pointer if you don't
/// want to specify a description. The pattern is a [glob
/// pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that is used
/// to filter the files. The pattern generally only supports `*` wildcards,
/// not `?` or brackets. This may however differ between frontends.
/// Additionally `;` can't be used in Windows's native file dialog if it's
/// part of the pattern. Multiple patterns may be specified by separating
/// them with ASCII space characters. There are operating systems where glob
/// patterns are not supported. A best effort lookup of the fitting MIME
/// type may be used by a frontend on those operating systems instead. The
/// pointers need to point to valid UTF-8 encoded text with the respective
/// given length.
pub fn user_settings_add_file_select_name_filter(
key_ptr: *const u8,
key_len: usize,
description_ptr: *const u8,
description_len: usize,
pattern_ptr: *const u8,
pattern_len: usize,
);
/// Adds a filter to a file select setting. The key needs to match the key
/// of the file select setting that it's supposed to be added to. The MIME
/// type is what's used to filter the files. Most operating systems do not
/// support MIME types, but the frontends are encouraged to look up the file
/// extensions that are associated with the MIME type and use those as a
/// filter in those cases. You may also use wildcards as part of the MIME
/// types such as `image/*`. The support likely also varies between
/// frontends however. The pointers need to point to valid UTF-8 encoded
/// text with the respective given length.
pub fn user_settings_add_file_select_mime_filter(
key_ptr: *const u8,
key_len: usize,
mime_type_ptr: *const u8,
mime_type_len: usize,
);
/// Adds a tooltip to a setting based on its key. A tooltip is useful for
/// explaining the purpose of a setting to the user. The pointers need to
/// point to valid UTF-8 encoded text with the respective given length.
Expand Down
53 changes: 53 additions & 0 deletions crates/livesplit-auto-splitting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,58 @@
//! option_description_ptr: *const u8,
//! option_description_len: usize,
//! ) -> bool;
//! /// Adds a new file select setting that the user can modify. This allows the
//! /// user to choose a file from the file system. The key is used to store the
//! /// path of the file in the settings map and needs to be unique across all
//! /// types of settings. The description is what's shown to the user. The
//! /// pointers need to point to valid UTF-8 encoded text with the respective
//! /// given length. The path is a path that is accessible through the WASI
//! /// file system, so a Windows path of `C:\foo\bar.exe` would be stored as
//! /// `/mnt/c/foo/bar.exe`.
//! pub fn user_settings_add_file_select(
//! key_ptr: *const u8,
//! key_len: usize,
//! description_ptr: *const u8,
//! description_len: usize,
//! );
//! /// Adds a filter to a file select setting. The key needs to match the key
//! /// of the file select setting that it's supposed to be added to. The
//! /// description is what's shown to the user for the specific filter. The
//! /// description is optional. You may provide a null pointer if you don't
//! /// want to specify a description. The pattern is a [glob
//! /// pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that is used
//! /// to filter the files. The pattern generally only supports `*` wildcards,
//! /// not `?` or brackets. This may however differ between frontends.
//! /// Additionally `;` can't be used in Windows's native file dialog if it's
//! /// part of the pattern. Multiple patterns may be specified by separating
//! /// them with ASCII space characters. There are operating systems where glob
//! /// patterns are not supported. A best effort lookup of the fitting MIME
//! /// type may be used by a frontend on those operating systems instead. The
//! /// pointers need to point to valid UTF-8 encoded text with the respective
//! /// given length.
//! pub fn user_settings_add_file_select_name_filter(
//! key_ptr: *const u8,
//! key_len: usize,
//! description_ptr: *const u8,
//! description_len: usize,
//! pattern_ptr: *const u8,
//! pattern_len: usize,
//! );
//! /// Adds a filter to a file select setting. The key needs to match the key
//! /// of the file select setting that it's supposed to be added to. The MIME
//! /// type is what's used to filter the files. Most operating systems do not
//! /// support MIME types, but the frontends are encouraged to look up the file
//! /// extensions that are associated with the MIME type and use those as a
//! /// filter in those cases. You may also use wildcards as part of the MIME
//! /// types such as `image/*`. The support likely also varies between
//! /// frontends however. The pointers need to point to valid UTF-8 encoded
//! /// text with the respective given length.
//! pub fn user_settings_add_file_select_mime_filter(
//! key_ptr: *const u8,
//! key_len: usize,
//! mime_type_ptr: *const u8,
//! mime_type_len: usize,
//! );
//! /// Adds a tooltip to a setting based on its key. A tooltip is useful for
//! /// explaining the purpose of a setting to the user. The pointers need to
//! /// point to valid UTF-8 encoded text with the respective given length.
Expand Down Expand Up @@ -503,6 +555,7 @@ mod process;
mod runtime;
pub mod settings;
mod timer;
pub mod wasi_path;

pub use process::Process;
pub use runtime::{
Expand Down
33 changes: 4 additions & 29 deletions crates/livesplit-auto-splitting/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use std::{
io,
path::{self, Path},
time::{Duration, Instant},
};

Expand All @@ -11,7 +10,7 @@ use read_process_memory::{CopyAddress, ProcessHandle};
use snafu::{OptionExt, ResultExt, Snafu};
use sysinfo::{self, PidExt, ProcessExt};

use crate::runtime::ProcessList;
use crate::{runtime::ProcessList, wasi_path};

#[derive(Debug, Snafu)]
#[snafu(context(suffix(false)))]
Expand Down Expand Up @@ -70,7 +69,7 @@ impl Process {
.max_by_key(|p| (p.start_time(), p.pid().as_u32()))
.context(ProcessDoesntExist)?;

let path = build_path(process.exe());
let path = wasi_path::from_native(process.exe());

let pid = process.pid().as_u32() as Pid;

Expand All @@ -93,7 +92,7 @@ impl Process {
.get(sysinfo::Pid::from_u32(pid))
.context(ProcessDoesntExist)?;

let path = build_path(process.exe());
let path = wasi_path::from_native(process.exe());

let pid_out = pid as Pid;

Expand Down Expand Up @@ -155,7 +154,7 @@ impl Process {
.iter()
.find(|m| m.filename().is_some_and(|f| f.ends_with(module)))
.context(ModuleDoesntExist)
.map(|m| build_path(m.filename().unwrap()).unwrap_or_default())
.map(|m| wasi_path::from_native(m.filename().unwrap()).unwrap_or_default())
}

pub(super) fn read_mem(&self, address: Address, buf: &mut [u8]) -> io::Result<()> {
Expand Down Expand Up @@ -239,27 +238,3 @@ impl Process {
Ok(())
}
}

pub fn build_path(original_path: &Path) -> Option<Box<str>> {
let mut path = String::from("/mnt");
for component in original_path.components() {
if !path.ends_with('/') {
path.push('/');
}
match component {
path::Component::Prefix(prefix) => match prefix.kind() {
path::Prefix::VerbatimDisk(disk) | path::Prefix::Disk(disk) => {
path.push(disk.to_ascii_lowercase() as char)
}
_ => return None,
},
path::Component::Normal(c) => {
path.push_str(c.to_str()?);
}
path::Component::RootDir => {}
path::Component::CurDir => path.push('.'),
path::Component::ParentDir => path.push_str(".."),
}
}
Some(path.into_boxed_str())
}
82 changes: 82 additions & 0 deletions crates/livesplit-auto-splitting/src/runtime/api/user_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,88 @@ pub fn bind<T: Timer>(linker: &mut Linker<Context<T>>) -> Result<(), CreationErr
source,
name: "user_settings_add_choice_option",
})?
.func_wrap("env", "user_settings_add_file_select", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
key_len: u32,
description_ptr: u32,
description_len: u32| {
let (memory, context) = memory_and_context(&mut caller);
let key = get_str(memory, key_ptr, key_len)?.into();
let description = get_str(memory, description_ptr, description_len)?.into();
Arc::make_mut(&mut context.settings_widgets).push(settings::Widget {
key,
description,
tooltip: None,
kind: settings::WidgetKind::FileSelect {
filters: Arc::new(Vec::new()),
},
});
Ok(())
}
})
.map_err(|source| CreationError::LinkFunction {
source,
name: "user_settings_add_file_select",
})?
.func_wrap("env", "user_settings_add_file_select_name_filter", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
key_len: u32,
description_ptr: u32,
description_len: u32,
pattern_ptr: u32,
pattern_len: u32| {
let (memory, context) = memory_and_context(&mut caller);
let key = get_str(memory, key_ptr, key_len)?.into();
let description = if description_ptr != 0 {
Some(get_str(memory, description_ptr, description_len)?.into())
} else {
None
};
let pattern = get_str(memory, pattern_ptr, pattern_len)?.into();
let setting = Arc::make_mut(&mut context.settings_widgets)
.iter_mut()
.find(|s| s.key == key)
.context("There is no setting with the provided key.")?;
let settings::WidgetKind::FileSelect { filters } = &mut setting.kind else {
bail!("The setting is not a file select.");
};
Arc::make_mut(filters).push(settings::FileFilter::Name {
description,
pattern,
});
Ok(())
}
})
.map_err(|source| CreationError::LinkFunction {
source,
name: "user_settings_add_file_select_name_filter",
})?
.func_wrap("env", "user_settings_add_file_select_mime_filter", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
key_len: u32,
mime_ptr: u32,
mime_len: u32| {
let (memory, context) = memory_and_context(&mut caller);
let key = get_str(memory, key_ptr, key_len)?.into();
let mime = get_str(memory, mime_ptr, mime_len)?.into();
let setting = Arc::make_mut(&mut context.settings_widgets)
.iter_mut()
.find(|s| s.key == key)
.context("There is no setting with the provided key.")?;
let settings::WidgetKind::FileSelect { filters } = &mut setting.kind else {
bail!("The setting is not a file select.");
};
Arc::make_mut(filters).push(settings::FileFilter::MimeType(mime));
Ok(())
}
})
.map_err(|source| CreationError::LinkFunction {
source,
name: "user_settings_add_file_select_mime_filter",
})?
.func_wrap("env", "user_settings_set_tooltip", {
|mut caller: Caller<'_, Context<T>>,
key_ptr: u32,
Expand Down
4 changes: 2 additions & 2 deletions crates/livesplit-auto-splitting/src/runtime/api/wasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ use wasi_common::{
};
use wasmtime_wasi::{ambient_authority, WasiCtxBuilder};

use crate::process::build_path;
use crate::wasi_path;

pub fn build(script_path: Option<&Path>) -> WasiCtx {
let mut wasi = WasiCtxBuilder::new().build();

if let Some(script_path) = script_path {
if let Some(path) = build_path(script_path) {
if let Some(path) = wasi_path::from_native(script_path) {
let _ = wasi.push_env("SCRIPT_PATH", &path);
}
}
Expand Down
36 changes: 36 additions & 0 deletions crates/livesplit-auto-splitting/src/settings/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,42 @@ pub enum WidgetKind {
/// The available options for the setting.
options: Arc<Vec<ChoiceOption>>,
},
/// A file selection. This could be a button that opens a File Dialog.
FileSelect {
/// The filters that are used to filter the files that can be selected.
filters: Arc<Vec<FileFilter>>,
},
}

/// A filter for a file selection setting.
#[derive(Clone)]
pub enum FileFilter {
/// A filter that matches on the name of the file.
Name {
/// The description is what's shown to the user for the specific filter.
description: Option<Arc<str>>,
/// The pattern is a [glob
/// pattern](https://en.wikipedia.org/wiki/Glob_(programming)) that is
/// used to filter the files. The pattern generally only supports `*`
/// wildcards, not `?` or brackets. This may however differ between
/// frontends. Additionally `;` can't be used in Windows's native file
/// dialog if it's part of the pattern. Multiple patterns may be
/// specified by separating them with ASCII space characters. There are
/// operating systems where glob patterns are not supported. A best
/// effort lookup of the fitting MIME type may be used by a frontend on
/// those operating systems instead. The
/// [`mime_guess`](https://docs.rs/mime_guess) crate offers such a
/// lookup.
pattern: Arc<str>,
},
/// A filter that matches on the MIME type of the file. Most operating
/// systems do not support MIME types, but the frontends are encouraged to
/// look up the file extensions that are associated with the MIME type and
/// use those as a filter in those cases. You may also use wildcards as part
/// of the MIME types such as `image/*`. The support likely also varies
/// between frontends however. The
/// [`mime_guess`](https://docs.rs/mime_guess) crate offers such a lookup.
MimeType(Arc<str>),
}

/// An option for a choice setting.
Expand Down
32 changes: 32 additions & 0 deletions crates/livesplit-auto-splitting/src/settings/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ impl Map {
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}

/// Returns [`true`] if the identity of the map is the same as the identity
/// of the other map. Maps use the copy-on-write principle. This means that
/// cloning a map is cheap because it references all the same data as the
/// original until one of the variables is changed. With this function you
/// can check if two variables internally share the same data and are
/// therefore identical. This is useful to determine if the map has changed
/// since the last time it was checked. You may use this as part of a
/// compare-and-swap loop.
#[must_use]
#[inline]
pub fn is_unchanged(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.values, &other.values)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -152,4 +166,22 @@ mod tests {
map.insert("c".into(), Value::Bool(true));
assert!(!map.is_empty());
}

#[test]
fn test_is_unchanged() {
let mut map = Map::new();
let mut map2 = map.clone();
assert!(map.is_unchanged(&map2));
map.insert("a".into(), Value::Bool(true));
assert!(!map.is_unchanged(&map2));
map2.insert("a".into(), Value::Bool(true));
assert!(!map.is_unchanged(&map2));
map.insert("b".into(), Value::Bool(false));
assert!(!map.is_unchanged(&map2));
map2.insert("b".into(), Value::Bool(false));
assert!(!map.is_unchanged(&map2));
map2 = map.clone();
assert!(map.is_unchanged(&map2));
assert!(map2.is_unchanged(&map));
}
}
Loading
Loading