Start with defaults when configuration is invalid (#29)
Invalid configuration currently prevents login. Ferese now starts with built-in defaults and shows a notification when the shell is ready, leaving the file untouched. Loading and validation belong to the config module, while the theme module resolves appearance. Validation remains strict, and failed live reloads keep the active settings. The native Morph regression checks the animated input region and idle frame scheduling while its backing surface stays allocated. Validation: workspace and Python tests, formatting, Clippy, release build, and isolated Wayland startup/reload and panel-motion tests. Native CI includes the startup regression. Related: #28.
This commit is contained in:
1 parent
b31618b749
commit
7a7df0106c
14 files changed
+456
-87
No files matched your search
@@ -2,6 +2,7 @@ mod appearance;
|
||||
mod bindings;
|
||||
mod input_motion;
|
||||
mod layout;
|
||||
mod loading;
|
||||
mod outputs;
|
||||
mod xwayland;
|
||||
|
||||
@@ -10,6 +11,7 @@ use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) use self::loading::{prepare, prepare_document, read_source, startup};
|
||||
pub(crate) use self::xwayland::{XwaylandConfig, XwaylandStartup};
|
||||
use appearance::*;
|
||||
pub use appearance::{BorderGradient, FocusEffectSettings, MaterialStyle, ThemeSettings};
|
||||
@@ -403,7 +405,7 @@ mod tests {
|
||||
.contains("unknown field")
|
||||
);
|
||||
assert!(
|
||||
crate::theme::prepare(&source, std::path::Path::new("/tmp"))
|
||||
crate::config::prepare(&source, std::path::Path::new("/tmp"))
|
||||
.err()
|
||||
.unwrap()
|
||||
.contains(&key.replace('-', "_"))
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
|
||||
use ferese_config::Document;
|
||||
use ferese_config::theme::Candidate;
|
||||
|
||||
use super::Config;
|
||||
use crate::RuntimeConfig;
|
||||
|
||||
pub(crate) fn read_source(path: &Path) -> Result<String, String> {
|
||||
let mut source = String::new();
|
||||
std::fs::File::open(path)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?
|
||||
.take(60 * 1024 + 1)
|
||||
.read_to_string(&mut source)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if source.len() > 60 * 1024 {
|
||||
return Err(format!("{} exceeds 60 KiB", path.display()));
|
||||
}
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(source: &str, directory: &Path) -> Result<(Config, RuntimeConfig, Candidate), String> {
|
||||
let document = Document::parse(source).map_err(|e| e.to_string())?;
|
||||
prepare_document(&document, directory)
|
||||
}
|
||||
|
||||
pub(crate) struct StartupConfig {
|
||||
pub(crate) source: Option<String>,
|
||||
pub(crate) runtime: RuntimeConfig,
|
||||
pub(crate) theme: Candidate,
|
||||
pub(crate) error: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn startup(path: Option<&Path>) -> Result<StartupConfig, String> {
|
||||
let directory = path.and_then(Path::parent).unwrap_or(Path::new("."));
|
||||
let source = path.map_or(Ok(None), |path| match std::fs::symlink_metadata(path) {
|
||||
Ok(_) => read_source(path).map(Some),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(error) => Err(format!("{}: {error}", path.display())),
|
||||
});
|
||||
let prepared = source.and_then(|source| {
|
||||
let (_, runtime, theme) = prepare(source.as_deref().unwrap_or(""), directory)?;
|
||||
Ok(StartupConfig {
|
||||
source,
|
||||
runtime,
|
||||
theme,
|
||||
error: None,
|
||||
})
|
||||
});
|
||||
match prepared {
|
||||
Ok(config) => Ok(config),
|
||||
Err(error) => {
|
||||
let (_, runtime, theme) = prepare("", directory)?;
|
||||
Ok(StartupConfig {
|
||||
source: Some(String::new()),
|
||||
runtime,
|
||||
theme,
|
||||
error: Some(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_document(
|
||||
document: &Document,
|
||||
directory: &Path,
|
||||
) -> Result<(Config, RuntimeConfig, Candidate), String> {
|
||||
let candidate = crate::theme::resolve_candidate(document, directory)?;
|
||||
let config: Config =
|
||||
serde_json::from_value(document.with_theme(&candidate.theme).value().clone()).map_err(|e| e.to_string())?;
|
||||
let mut runtime = config.runtime_config().map_err(|e| e.to_string())?;
|
||||
runtime.theme_settings = Config::resolved_theme_settings(&candidate.theme).map_err(|e| e.to_string())?;
|
||||
Ok((config, runtime, candidate))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn invalid_startup_uses_defaults_and_keeps_the_authored_file() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("config.kdl");
|
||||
let (_, defaults, default_theme) = prepare("", directory.path()).unwrap();
|
||||
for source in [
|
||||
"panel main { surface solid; group-surface none; }",
|
||||
"panel main { geometry { height 1; }; }",
|
||||
"animations { speed 0; }",
|
||||
"theme { file \"missing-theme.kdl\"; }",
|
||||
"panel main {",
|
||||
] {
|
||||
std::fs::write(&path, source).unwrap();
|
||||
let config = startup(Some(&path)).unwrap();
|
||||
assert!(config.error.is_some(), "{source}");
|
||||
assert_eq!(config.source.as_deref(), Some(""));
|
||||
assert_eq!(config.runtime.panel_style, defaults.panel_style);
|
||||
assert_eq!(config.runtime.layout_mode, defaults.layout_mode);
|
||||
assert_eq!(config.theme.theme, default_theme.theme);
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), source);
|
||||
assert!(
|
||||
prepare(source, directory.path()).is_err(),
|
||||
"strict validation must still reject {source}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_distinguishes_missing_valid_and_unreadable_files() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("config.kdl");
|
||||
let missing = startup(Some(&path)).unwrap();
|
||||
assert!(missing.error.is_none());
|
||||
assert!(missing.source.is_none());
|
||||
assert!(!path.exists());
|
||||
|
||||
let source = "// keep me\npanel main { style floating; geometry { height 42; }; }";
|
||||
std::fs::write(&path, source).unwrap();
|
||||
let valid = startup(Some(&path)).unwrap();
|
||||
assert!(valid.error.is_none());
|
||||
assert_eq!(valid.source.as_deref(), Some(source));
|
||||
assert_eq!(valid.runtime.panel_style, ferese_panel::PanelStyle::Floating);
|
||||
assert_eq!(
|
||||
Document::parse(valid.source.as_deref().unwrap())
|
||||
.unwrap()
|
||||
.get("panels.0.geometry.height"),
|
||||
Some(&serde_json::json!(42))
|
||||
);
|
||||
assert_eq!(std::fs::read_to_string(&path).unwrap(), source);
|
||||
|
||||
std::fs::write(&path, [0xff]).unwrap();
|
||||
assert!(startup(Some(&path)).unwrap().error.is_some());
|
||||
assert_eq!(std::fs::read(&path).unwrap(), [0xff]);
|
||||
assert!(startup(Some(directory.path())).unwrap().error.is_some());
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
std::os::unix::fs::symlink(directory.path().join("missing"), &path).unwrap();
|
||||
assert!(startup(Some(&path)).unwrap().error.is_some());
|
||||
assert!(path.is_symlink());
|
||||
}
|
||||
}
|
||||
+19
-13
@@ -78,14 +78,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let launch =
|
||||
LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let initial_source = config::config_path()
|
||||
.filter(|path| path.exists())
|
||||
.map(|path| theme::read_source(&path))
|
||||
.transpose()?;
|
||||
let directory = config::config_path()
|
||||
.and_then(|path| path.parent().map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
let (_, runtime, candidate) = theme::prepare(initial_source.as_deref().unwrap_or(""), &directory)?;
|
||||
let config_path = config::config_path();
|
||||
let startup = config::startup(config_path.as_deref())?;
|
||||
if let Some(error) = &startup.error {
|
||||
warn!(%error, "configuration could not be loaded at startup; using defaults");
|
||||
}
|
||||
let mut event_loop = EventLoop::try_new()?;
|
||||
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM])?;
|
||||
event_loop
|
||||
@@ -99,8 +96,14 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
backends::BackendKind::Drm => ipc::Endpoint::desktop()?,
|
||||
backends::BackendKind::Nested => ipc::Endpoint::instance()?,
|
||||
};
|
||||
let mut state = Ferese::new_with_session(&mut event_loop, display, runtime, endpoint, launch.session_policy)?;
|
||||
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
||||
let mut state = Ferese::new_with_session(
|
||||
&mut event_loop,
|
||||
display,
|
||||
startup.runtime,
|
||||
endpoint,
|
||||
launch.session_policy,
|
||||
)?;
|
||||
state.config_source = startup.source;
|
||||
state.config_sections = state
|
||||
.config_source
|
||||
.as_deref()
|
||||
@@ -112,7 +115,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
value
|
||||
});
|
||||
theme::init(&mut event_loop, &mut state, candidate)?;
|
||||
if let Some(error) = startup.error {
|
||||
state.theme_engine.startup_failed(config_path.as_deref(), error);
|
||||
}
|
||||
theme::init(&mut event_loop, &mut state, startup.theme)?;
|
||||
let _resume_monitor = resume::init(&mut event_loop)?;
|
||||
idle_inhibition::media::init(&mut event_loop)?;
|
||||
overview::init_font_loader(&mut event_loop, &mut state)?;
|
||||
@@ -171,8 +177,8 @@ fn init_logging() {
|
||||
}
|
||||
|
||||
fn check_config(path: &std::path::Path) -> Result<(), String> {
|
||||
let source = theme::read_source(path)?;
|
||||
theme::prepare(&source, path.parent().unwrap_or(std::path::Path::new(".")))?;
|
||||
let source = config::read_source(path)?;
|
||||
config::prepare(&source, path.parent().unwrap_or(std::path::Path::new(".")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ impl Prepared {
|
||||
.map(|path| ferese_config::theme::theme_path(directory, path)),
|
||||
);
|
||||
}
|
||||
let (_, runtime, candidate) = crate::theme::prepare_document(&document, directory)?;
|
||||
let (_, runtime, candidate) = crate::config::prepare_document(&document, directory)?;
|
||||
let mut sections = document.value().clone();
|
||||
if let Some(object) = sections.as_object_mut() {
|
||||
object.remove("theme");
|
||||
@@ -229,7 +229,7 @@ impl Worker {
|
||||
|| request.fallback.ok_or_else(|| "No configuration available".into()),
|
||||
|path| {
|
||||
if path.exists() || !request.force {
|
||||
crate::theme::read_source(path)
|
||||
crate::config::read_source(path)
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
@@ -362,7 +362,7 @@ impl crate::Ferese {
|
||||
|
||||
pub(crate) fn reload_config(&mut self) -> Result<(), String> {
|
||||
let path = crate::config::config_path().ok_or("config directory is unavailable")?;
|
||||
self.reload_config_source(crate::theme::read_source(&path)?)
|
||||
self.reload_config_source(crate::config::read_source(&path)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ mod tests {
|
||||
return;
|
||||
}
|
||||
|
||||
let (_, runtime, _) = crate::theme::prepare("", std::path::Path::new(".")).unwrap();
|
||||
let (_, runtime, _) = crate::config::prepare("", std::path::Path::new(".")).unwrap();
|
||||
let mut event_loop = calloop::EventLoop::try_new().unwrap();
|
||||
let display = smithay::reexports::wayland_server::Display::new().unwrap();
|
||||
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
|
||||
@@ -498,7 +498,7 @@ mod tests {
|
||||
}
|
||||
let source = "appearance { focus-effect { inactive-opacity 0.8; inactive-dim 0.25; }; }";
|
||||
let directory = Directory::new();
|
||||
let (_, runtime, candidate) = crate::theme::prepare(source, &directory.0).unwrap();
|
||||
let (_, runtime, candidate) = crate::config::prepare(source, &directory.0).unwrap();
|
||||
let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let display = smithay::reexports::wayland_server::Display::new().unwrap();
|
||||
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
|
||||
@@ -566,7 +566,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let directory = Directory::new();
|
||||
let (_, runtime, _) = crate::theme::prepare("xwayland { enabled #false; }", &directory.0).unwrap();
|
||||
let (_, runtime, _) = crate::config::prepare("xwayland { enabled #false; }", &directory.0).unwrap();
|
||||
let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let display = smithay::reexports::wayland_server::Display::new().unwrap();
|
||||
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
|
||||
@@ -615,7 +615,7 @@ mod tests {
|
||||
}
|
||||
let directory = Directory::new();
|
||||
let source = "output-profile mobile { output eDP-1 scale=1.75; }";
|
||||
let (_, runtime, _) = crate::theme::prepare(source, &directory.0).unwrap();
|
||||
let (_, runtime, _) = crate::config::prepare(source, &directory.0).unwrap();
|
||||
let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let display = smithay::reexports::wayland_server::Display::new().unwrap();
|
||||
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
|
||||
|
||||
@@ -697,7 +697,8 @@ mod region_geometry_tests {
|
||||
let source = format!(
|
||||
"theme {{ material {{ style translucent; opacity 0.7; }}; accessibility {{ reduce-transparency #{reduce}; }}; }}; panel main {{ style {surface}; background-opacity {opacity}; }}"
|
||||
);
|
||||
let (_, runtime, candidate) = crate::theme::prepare(&source, std::path::Path::new("/tmp")).unwrap();
|
||||
let (_, runtime, candidate) =
|
||||
crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap();
|
||||
let material = resolve_surface_material(
|
||||
SemanticRole::Panel,
|
||||
runtime.theme_settings,
|
||||
@@ -736,7 +737,7 @@ mod region_geometry_tests {
|
||||
let source = format!(
|
||||
"theme {{ material {{ style translucent; opacity 0.6; blur-radius 20; }}; accessibility {{ reduce-transparency #{reduce}; }}; }}; panel main {{ style morph; }}"
|
||||
);
|
||||
let (_, runtime, candidate) = crate::theme::prepare(&source, std::path::Path::new("/tmp")).unwrap();
|
||||
let (_, runtime, candidate) = crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap();
|
||||
let material =
|
||||
resolve_surface_material(SemanticRole::Panel, runtime.theme_settings, None, runtime.panel_style);
|
||||
assert_eq!(material.shadow, [0.; 3]);
|
||||
|
||||
@@ -273,11 +273,11 @@ fn check_config_rejects_an_otherwise_valid_oversized_file() {
|
||||
let valid = format!("//{}\n", "x".repeat(60 * 1024 - 3));
|
||||
std::fs::write(&path, &valid).unwrap();
|
||||
crate::check_config(&path).unwrap();
|
||||
assert!(crate::theme::prepare(&(valid.clone() + "\n"), directory.path()).is_ok());
|
||||
assert!(crate::config::prepare(&(valid.clone() + "\n"), directory.path()).is_ok());
|
||||
std::fs::write(&path, valid + "\n").unwrap();
|
||||
let error = crate::check_config(&path).unwrap_err();
|
||||
assert!(error.contains("exceeds 60 KiB"), "{error}");
|
||||
assert_eq!(crate::theme::read_source(&path).unwrap_err(), error);
|
||||
assert_eq!(crate::config::read_source(&path).unwrap_err(), error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+50
-34
@@ -1,5 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{self, Read};
|
||||
use std::io;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
@@ -20,9 +20,9 @@ use serde_json::{Value, json};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::output::Output;
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::config::Config;
|
||||
use crate::wallpaper::{WallpaperConfig, WallpaperState};
|
||||
use crate::{Ferese, RuntimeConfig};
|
||||
|
||||
const DEBOUNCE: Duration = Duration::from_millis(150);
|
||||
const WALLPAPER_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
@@ -35,34 +35,14 @@ fn transition_progress(elapsed: Duration, duration: Duration) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_source(path: &Path) -> Result<String, String> {
|
||||
let mut source = String::new();
|
||||
std::fs::File::open(path)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))?
|
||||
.take(60 * 1024 + 1)
|
||||
.read_to_string(&mut source)
|
||||
.map_err(|e| e.to_string())?;
|
||||
if source.len() > 60 * 1024 {
|
||||
return Err(format!("{} exceeds 60 KiB", path.display()));
|
||||
}
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare(source: &str, directory: &Path) -> Result<(Config, RuntimeConfig, Candidate), String> {
|
||||
let document = Document::parse(source).map_err(|e| e.to_string())?;
|
||||
prepare_document(&document, directory)
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_document(
|
||||
document: &Document,
|
||||
directory: &Path,
|
||||
) -> Result<(Config, RuntimeConfig, Candidate), String> {
|
||||
let candidate = resolve_with_context(document, directory, Timestamp::now(), &auto_context(), read_source)?;
|
||||
let config: Config =
|
||||
serde_json::from_value(document.with_theme(&candidate.theme).value().clone()).map_err(|e| e.to_string())?;
|
||||
let mut runtime = config.runtime_config().map_err(|e| e.to_string())?;
|
||||
runtime.theme_settings = Config::resolved_theme_settings(&candidate.theme).map_err(|e| e.to_string())?;
|
||||
Ok((config, runtime, candidate))
|
||||
pub(crate) fn resolve_candidate(document: &Document, directory: &Path) -> Result<Candidate, String> {
|
||||
resolve_with_context(
|
||||
document,
|
||||
directory,
|
||||
Timestamp::now(),
|
||||
&auto_context(),
|
||||
crate::config::read_source,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn preview(args: &Value) -> Result<Value, String> {
|
||||
@@ -76,7 +56,7 @@ pub(crate) fn preview(args: &Value) -> Result<Value, String> {
|
||||
.or_else(|| crate::config::config_path().and_then(|path| path.parent().map(ToOwned::to_owned)))
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
let document = Document::parse(source).map_err(|e| e.to_string())?;
|
||||
let candidate = resolve_with_context(&document, &directory, Timestamp::now(), &auto_context(), read_source)?;
|
||||
let candidate = resolve_candidate(&document, &directory)?;
|
||||
Ok(
|
||||
json!({"theme": candidate.theme, "warnings": candidate.warnings, "families": candidate.families, "fallback_note": candidate.fallback_note}),
|
||||
)
|
||||
@@ -165,6 +145,7 @@ pub(crate) struct Engine {
|
||||
pending: Option<Pending>,
|
||||
transition: Option<Transition>,
|
||||
last_frame: Option<Instant>,
|
||||
startup_rejection: Option<String>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -207,7 +188,21 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn startup_failed(&mut self, path: Option<&Path>, error: String) {
|
||||
let path = path
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "configuration".into());
|
||||
self.live.error = Some(format!(
|
||||
"Ferese started with default settings because {path} could not be loaded. Your file has not been changed.\n\n{error}"
|
||||
));
|
||||
self.startup_rejection = Some(error);
|
||||
}
|
||||
|
||||
pub(crate) fn reject(&mut self, error: String) {
|
||||
if self.startup_rejection.as_ref() == Some(&error) {
|
||||
return;
|
||||
}
|
||||
self.startup_rejection = None;
|
||||
if self.live.error.as_ref() != Some(&error) {
|
||||
tracing::warn!(%error, "theme reload rejected; retaining active theme");
|
||||
self.live.error = Some(error);
|
||||
@@ -400,6 +395,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn accept_theme(&mut self, candidate: Candidate, wallpaper: WallpaperConfig) {
|
||||
self.theme_engine.startup_rejection = None;
|
||||
self.theme_engine.active_files = candidate.files.clone();
|
||||
self.theme_engine.watch_files(&candidate.files);
|
||||
if let Err(error) = self.theme_engine.arm_clock(candidate.next_transition) {
|
||||
@@ -584,11 +580,11 @@ impl Ferese {
|
||||
|
||||
pub(crate) fn set_theme_mode(&mut self, mode: Mode) -> Result<Value, String> {
|
||||
let path = crate::config::config_path().ok_or("No configuration directory")?;
|
||||
let original = read_source(&path)?;
|
||||
let original = crate::config::read_source(&path)?;
|
||||
let mut document = Document::parse(&original).map_err(|e| e.to_string())?;
|
||||
document.set("theme.mode", json!(mode)).map_err(|e| e.to_string())?;
|
||||
let source = document.to_string();
|
||||
prepare(&source, path.parent().unwrap_or(Path::new(".")))?;
|
||||
crate::config::prepare(&source, path.parent().unwrap_or(Path::new(".")))?;
|
||||
let temporary = path.with_extension(format!("kdl.{}.tmp", std::process::id()));
|
||||
let result = (|| -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
@@ -603,7 +599,7 @@ impl Ferese {
|
||||
}
|
||||
file.write_all(source.as_bytes()).map_err(|e| e.to_string())?;
|
||||
file.sync_all().map_err(|e| e.to_string())?;
|
||||
if read_source(&path)? != original {
|
||||
if crate::config::read_source(&path)? != original {
|
||||
return Err("Configuration changed while updating the mode; please try again".into());
|
||||
}
|
||||
std::fs::rename(&temporary, &path).map_err(|e| e.to_string())?;
|
||||
@@ -622,6 +618,26 @@ impl Ferese {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn startup_warning_survives_repeated_rejection_without_duplicate_publication() {
|
||||
let mut engine = Engine::default();
|
||||
engine.startup_failed(Some(Path::new("/config.kdl")), "unknown field group_surface".into());
|
||||
engine.publish();
|
||||
let initial = engine.value();
|
||||
assert!(initial["error"].as_str().unwrap().contains("default settings"));
|
||||
assert!(initial["error"].as_str().unwrap().contains("/config.kdl"));
|
||||
for _ in 0..3 {
|
||||
engine.reject("unknown field group_surface".into());
|
||||
assert_eq!(engine.value(), initial);
|
||||
}
|
||||
engine.reject("different invalid edit".into());
|
||||
assert_eq!(engine.live.error.as_deref(), Some("different invalid edit"));
|
||||
assert_eq!(engine.revision, 2);
|
||||
engine.reject("unknown field group_surface".into());
|
||||
assert_eq!(engine.live.error.as_deref(), Some("unknown field group_surface"));
|
||||
assert_eq!(engine.revision, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_projects_live_values_and_preserves_revision_semantics() {
|
||||
let mut engine = Engine::default();
|
||||
|
||||
@@ -646,7 +646,7 @@ mod manager_loop {
|
||||
|
||||
/// A `Ferese` wired to a real event loop, ready to run the manager.
|
||||
fn session(directory: &Path, path: PathBuf) -> (EventLoop<'static, Ferese>, Ferese) {
|
||||
let (_, runtime, _) = crate::theme::prepare("xwayland { }", directory).expect("prepare configuration");
|
||||
let (_, runtime, _) = crate::config::prepare("xwayland { }", directory).expect("prepare configuration");
|
||||
// Owned inputs, so this test never competes for a real display slot.
|
||||
let allocator = directory.join("x11");
|
||||
fs::create_dir_all(&allocator).expect("create the allocator directory");
|
||||
|
||||
Reference in new issue
Block a user