Skip to content

Commit

Permalink
Merge pull request #748 from AlexKnauth/file-selection
Browse files Browse the repository at this point in the history
This adds a `FileSelect` Widget to the Auto Splitting Runtime.

The `asr-debugger` and `LiveSplit.AutoSplittingRuntime` companion branches both implement this as a button in the settings Gui that opens a file dialog when pressed. The user can select a file, and the WASI-compatible path to that file gets stored in the settings map.

The `asr` companion branch adds a `FileSelect` type that implements the `Widget` trait such that an auto-splitter can put it in their settings gui like this:
```rust
#[derive(Gui)]
pub struct SettingsGui {
    #[filter(
        // File name patterns with names
        ("PNG images", "*.png"),
        // Multiple patterns separated by space
        ("Rust files", "*.rs Cargo.*"),
        // The name is optional
        (_, "*.md"),
        // MIME types
        "text/plain",
        // MIME types with wildcards
        "image/*",
    )]
    text_file: FileSelect,
}
```
  • Loading branch information
CryZe authored Dec 21, 2023
2 parents 754799c + 4853084 commit 3ac9c0d
Show file tree
Hide file tree
Showing 9 changed files with 414 additions and 31 deletions.
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

0 comments on commit 3ac9c0d

Please sign in to comment.