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

Watch file system for changes #9

Merged
merged 7 commits into from
Nov 6, 2021
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
337 changes: 302 additions & 35 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ env_logger = { version = "0.9", default-features = false, features = ["atty", "h
epaint = { version = "0.15", default-features = false, features = ["single_threaded"] }
font-loader = "0.11"
human-sort = "0.2"
hotwatch = "0.4"
kuchiki = "0.8"
log = "0.4"
ordered-multimap = "0.4"
Expand All @@ -43,6 +44,9 @@ winit_input_helper = "0.10"
[target.'cfg(windows)'.build-dependencies]
embed-resource = "1.6"

[dev-dependencies]
tempfile = "3.2"

[profile.release]
codegen-units = 1
lto = true
Expand Down
15 changes: 9 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ impl Config {

/// Get window configuration if it's valid.
pub(crate) fn get_window(&self) -> Option<Window> {
let window = &self.doc["window"];
let window = &self.doc.get("window")?;

let x = window["x"].as_integer()?;
let y = window["y"].as_integer()?;
let x = window.get("x").and_then(|t| t.as_integer())?;
let y = window.get("y").and_then(|t| t.as_integer())?;
let position = PhysicalPosition::new(x as i32, y as i32);

let width = window["width"].as_integer()?;
let height = window["height"].as_integer()?;
let width = window.get("width").and_then(|t| t.as_integer())?;
let height = window.get("height").and_then(|t| t.as_integer())?;
let size = PhysicalSize::new(
(width as u32).max(self.min_size.width),
(height as u32).max(self.min_size.height),
Expand All @@ -221,7 +221,10 @@ impl Config {

/// Update the setup exports path.
pub(crate) fn update_setups_path<P: AsRef<Path>>(&mut self, setups_path: P) {
self.setups_path = setups_path.as_ref().to_path_buf();
self.setups_path = setups_path
.as_ref()
.canonicalize()
.unwrap_or_else(|_| setups_path.as_ref().to_path_buf());

// Note that to_string_lossy() is destructive when the path contains invalid UTF-8 sequences.
// If this is a problem in practice, we _could_ write unencodable paths as an array of
Expand Down
10 changes: 9 additions & 1 deletion src/framework.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub(crate) enum Error {
}

/// User event handling is performed with this type.
#[derive(Debug, Eq, PartialEq)]
#[derive(Debug)]
pub(crate) enum UserEvent {
/// Configuration error handling events
ConfigHandler(ConfigHandler),
Expand All @@ -46,6 +46,9 @@ pub(crate) enum UserEvent {
/// Change the path for setup export files.
SetupPath(Option<PathBuf>),

/// File system event for the setup export path.
FsChange(hotwatch::Event),

/// Change the theme preference.
Theme(UserTheme),
}
Expand Down Expand Up @@ -103,6 +106,11 @@ impl Framework {
self.egui_state.on_event(&self.egui_ctx, event);
}

/// Handle file system change events.
pub(crate) fn handle_fs_change(&mut self, event: hotwatch::Event) {
self.gui.handle_fs_change(event);
}

/// Resize egui.
pub(crate) fn resize(&mut self, size: PhysicalSize<u32>) {
self.screen_descriptor.physical_width = size.width;
Expand Down
135 changes: 121 additions & 14 deletions src/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ use self::grid::SetupGrid;
use crate::config::{Config, UserTheme};
use crate::framework::UserEvent;
use crate::setup::{Setup, Setups};
use crate::str_ext::Ellipsis;
use crate::str_ext::{Ellipsis, HumanCompare};
use copypasta::{ClipboardContext, ClipboardProvider};
use egui::widgets::color_picker::{color_edit_button_srgba, Alpha};
use egui::{CtxRef, Widget};
use hotwatch::Hotwatch;
use std::collections::{HashMap, VecDeque};
use std::path::Path;
use std::time::{Duration, Instant};
use thiserror::Error;
use winit::event_loop::EventLoopProxy;

mod grid;
Expand All @@ -23,6 +25,9 @@ pub(crate) struct Gui {
/// A tree of `Setups` containing all known setup exports.
setups: Setups,

/// Filesystem watcher for changes to any setup exports.
hotwatch: Hotwatch,

/// Selected track name.
selected_track_name: Option<String>,

Expand Down Expand Up @@ -85,6 +90,12 @@ pub(crate) struct ShowWarning {
context: String,
}

#[derive(Debug, Error)]
pub(crate) enum Error {
#[error("File system watch error: {0}")]
Notify(#[from] hotwatch::Error),
}

impl Gui {
/// Create a GUI.
pub(crate) fn new(
Expand All @@ -93,10 +104,16 @@ impl Gui {
event_loop_proxy: EventLoopProxy<UserEvent>,
show_errors: VecDeque<ShowError>,
show_warnings: VecDeque<ShowWarning>,
) -> Self {
Self {
) -> Result<Self, Error> {
let mut hotwatch = Hotwatch::new()?;
let watcher = Self::watch_setups_path(event_loop_proxy.clone());

hotwatch.watch(config.get_setups_path(), watcher)?;

Ok(Self {
config,
setups,
hotwatch,
selected_track_name: None,
selected_car_name: None,
selected_setups: Vec::new(),
Expand All @@ -107,7 +124,7 @@ impl Gui {
show_errors,
show_warnings,
show_tooltips: HashMap::new(),
}
})
}

/// Draw the UI using egui.
Expand Down Expand Up @@ -208,11 +225,104 @@ impl Gui {
}
}

/// Create a file system watcher.
fn watch_setups_path(event_loop_proxy: EventLoopProxy<UserEvent>) -> impl Fn(hotwatch::Event) {
move |event| {
event_loop_proxy
.send_event(UserEvent::FsChange(event))
.expect("Event loop must exist");
}
}

/// Handle file system change events.
///
/// Called by the closure from `Self::watch_setups_path`.
pub(crate) fn handle_fs_change(&mut self, event: hotwatch::Event) {
use crate::setup::UpdateKind::*;

// Update the setups tree.
let updates = self.setups.update(&event, &self.config);
for update in updates {
match update {
AddedSetup(track_name, car_name, index) => {
if self.selected_track_name.as_ref() == Some(&track_name)
&& self.selected_car_name.as_ref() == Some(&car_name)
{
// Update selected setups when a new one is added
for i in self.selected_setups.iter_mut() {
if *i >= index {
*i += 1;
}
}
}
}
RemovedSetup(track_name, car_name, index) => {
if self.selected_track_name.as_ref() == Some(&track_name)
&& self.selected_car_name.as_ref() == Some(&car_name)
{
// Update selected setups when an old one is removed
self.selected_setups.retain(|i| *i != index);
for i in self.selected_setups.iter_mut() {
if *i >= index {
*i -= 1;
}
}
}
}
RemovedCar(track_name, car_name) => {
if self.selected_track_name.as_ref() == Some(&track_name)
&& self.selected_car_name.as_ref() == Some(&car_name)
{
self.selected_car_name = None;
self.selected_setups.clear();
}
}
RemovedTrack(track_name) => {
if self.selected_track_name.as_ref() == Some(&track_name) {
self.selected_track_name = None;
self.selected_car_name = None;
self.selected_setups.clear();
}
}
}
}

// Show warning window if necessary.
if let hotwatch::Event::Error(error, path) = event {
let msg = path.map_or("Error while watching file system".to_string(), |path| {
format!("Error while watching path: `{:?}`", path)
});

self.show_warnings.push_front(ShowWarning::new(error, msg));
}
}

/// Update setups export path.
pub(crate) fn update_setups_path<P: AsRef<Path>>(&mut self, setups_path: P) {
if let Err(error) = self.hotwatch.unwatch(self.config.get_setups_path()) {
self.show_warnings.push_front(ShowWarning::new(
error,
format!(
"Unable to stop watching setup exports path for changes: `{:?}`",
self.config.get_setups_path()
),
));
}

self.config.update_setups_path(setups_path);
self.setups = Setups::new(&mut self.show_warnings, &self.config);
self.clear_filters();

let watcher = Self::watch_setups_path(self.event_loop_proxy.clone());
if let Err(error) = self.hotwatch.watch(self.config.get_setups_path(), watcher) {
self.show_warnings.push_front(ShowWarning::new(
error,
format!(
"Unable to watch setup exports path for changes: `{:?}`",
self.config.get_setups_path()
),
));
}
}

/// Clear track, car, and setup filters.
Expand All @@ -233,7 +343,7 @@ impl Gui {
};
track_selection.show_ui(ui, |ui| {
let mut track_names: Vec<_> = self.setups.tracks().keys().collect();
track_names.sort_unstable();
track_names.sort_unstable_by(|a, b| a.human_compare(b));

for track_name in track_names {
let checked = self.selected_track_name.as_ref() == Some(track_name);
Expand Down Expand Up @@ -282,7 +392,7 @@ impl Gui {
.expect("Invalid track name")
.keys()
.collect();
car_names.sort_unstable();
car_names.sort_unstable_by(|a, b| a.human_compare(b));

for car_name in car_names {
let checked = self.selected_car_name.as_ref() == Some(car_name);
Expand Down Expand Up @@ -317,16 +427,13 @@ impl Gui {
output_track_name = track_name.as_str();
output_car_name = car_name.as_str();

let mut setups: Vec<_> = tracks
let setups = tracks
.get(track_name)
.expect("Invalid track name")
.get(car_name)
.expect("Invalid car name")
.iter()
.collect();
setups.sort_unstable_by(|(a, _), (b, _)| a.partial_cmp(b).unwrap());
.expect("Invalid car name");

for (i, (name, _)) in setups.iter().enumerate() {
for (i, info) in setups.iter().enumerate() {
let position = selected_setups.iter().position(|&v| v == i);
let mut checked = position.is_some();
let color = position
Expand All @@ -335,7 +442,7 @@ impl Gui {
.cloned()
.unwrap_or_else(|| ui.visuals().text_color());

let checkbox = egui::Checkbox::new(&mut checked, name)
let checkbox = egui::Checkbox::new(&mut checked, info.name())
.text_color(color)
.ui(ui);
if checkbox.clicked() {
Expand All @@ -348,7 +455,7 @@ impl Gui {
}

for i in selected_setups {
output.push(&setups[*i].1);
output.push(setups[*i].setup());
}
}
}
Expand Down
43 changes: 2 additions & 41 deletions src/gui/grid.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::setup::Setup;
use crate::str_ext::HumanCompare;
use epaint::Galley;
use std::cmp::Ordering;
use std::sync::Arc;
Expand Down Expand Up @@ -116,7 +117,7 @@ impl<'setup> SetupGrid<'setup> {
// Compute diff between `value` and first column
let color = colors.next().unwrap_or_else(|| ui.visuals().text_color());
let (color, background) = if let Some(first_value) = first_value.as_ref() {
match string_compare(&value, first_value) {
match value.human_compare(first_value) {
Ordering::Less => (ui.visuals().text_color(), Some(diff_colors.0)),
Ordering::Greater => (ui.visuals().text_color(), Some(diff_colors.1)),
Ordering::Equal => (color, None),
Expand Down Expand Up @@ -202,15 +203,6 @@ fn intersect_keys<'a>(mut all_keys: impl Iterator<Item = Vec<&'a str>>) -> Vec<&
output
}

fn string_compare(a: &str, b: &str) -> Ordering {
if a.starts_with('-') && b.starts_with('-') {
// Reverse parameter order when comparing negative numbers
human_sort::compare(b, a)
} else {
human_sort::compare(a, b)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -301,35 +293,4 @@ mod tests {
let keys = intersect_keys(list.into_iter());
assert!(keys.is_empty());
}

#[test]
fn test_string_compare_text() {
assert_eq!(string_compare("a", "b"), Ordering::Less);
assert_eq!(string_compare("ab", "abc"), Ordering::Less);
assert_eq!(string_compare("abc", "abc"), Ordering::Equal);
}

#[test]
fn test_string_compare_numbers() {
assert_eq!(string_compare("1", "1"), Ordering::Equal);
assert_eq!(string_compare("10", "10"), Ordering::Equal);
assert_eq!(string_compare("1", "10"), Ordering::Less);
assert_eq!(string_compare("10", "1"), Ordering::Greater);

assert_eq!(string_compare("1", "2"), Ordering::Less);
assert_eq!(string_compare("10", "2"), Ordering::Greater);
assert_eq!(string_compare("1", "-2"), Ordering::Greater);
assert_eq!(string_compare("10", "-2"), Ordering::Greater);
assert_eq!(string_compare("-1", "2"), Ordering::Less);
assert_eq!(string_compare("-10", "2"), Ordering::Less);
assert_eq!(string_compare("-1", "-2"), Ordering::Greater);
assert_eq!(string_compare("-10", "-2"), Ordering::Less);
}

#[test]
#[ignore = "Fractions are not yet supported"]
fn test_string_compare_fractions() {
assert_eq!(string_compare("3/8", "1/2"), Ordering::Less);
assert_eq!(string_compare("5/8", "1/2"), Ordering::Greater);
}
}
Loading