Skip to content

Commit

Permalink
Watch file system for changes (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
parasyte committed Nov 6, 2021
1 parent 4d7f45c commit 83955d2
Show file tree
Hide file tree
Showing 10 changed files with 1,662 additions and 845 deletions.
337 changes: 302 additions & 35 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
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
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
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
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
@@ -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);
}
}

0 comments on commit 83955d2

Please sign in to comment.