New workspaces grow in; config saves don't redraw everything or race; media peek skips the player in front

- A workspace Hyprland makes for a switch (createworkspacev2) grows into the row
  over the slide instead of appearing at full size; the window keeps the full
  room meanwhile.
- Every config.toml save restarted the colour Animation, and Animation::new
  runs its clock even from a colour to itself, so every window redrew for 0.4 s
  per save. The colours now only fade when they change.
- The file watch could read config.toml halfway through the shell's own save
  and reset settings to their defaults. Saves and rereads share one lock, the
  file is written to a new file and renamed over, and the shell's own saves
  are skipped by the watch.
- A song change or new player only pops the island when the focused window
  isn't that player's app (parse::same_app, tested).
This commit is contained in:
server2
2026-10-15 11:28:44 +02:00
parent 5d322b940d
commit 6399844731
5 changed files with 190 additions and 57 deletions
+52 -5
View File
@@ -505,6 +505,17 @@ static VALUES: LazyLock<RwLock<HashMap<String, Value>>> = LazyLock::new(Default:
// what was wrong in the file when it was last read, for the config window
static ERRORS: Mutex<Vec<String>> = Mutex::new(Vec::new());
/*
* config.toml as the shell last wrote or read it. every save and every reread holds
* this lock, so the file watch can't read a save halfway or undo a change it raced
* with (it once reset settings to their defaults that way), and skips the shell's own saves
*/
static WRITTEN: Mutex<String> = Mutex::new(String::new());
fn written() -> std::sync::MutexGuard<'static, String> {
WRITTEN.lock().unwrap_or_else(PoisonError::into_inner)
}
// ponytail: a lock and a hash lookup per call, a few hundred a frame; a snapshot struct if a profile ever shows it
fn get<T: Setting>(path: &str, default: impl FnOnce() -> T) -> T {
VALUES.read().unwrap_or_else(PoisonError::into_inner).get(path).and_then(T::read).unwrap_or_else(default)
@@ -520,17 +531,37 @@ pub fn option(path: &str) -> Option<&'static Opt> {
// at startup: config.toml, or the first time, one made from the old shell.conf
pub fn load() {
let mut written = written();
let text = fs::read_to_string(file()).unwrap_or_else(|_| {
migrate();
let text = render();
let _ = fs::write(file(), &text);
let _ = write_file(&text);
text
});
apply_text(&text);
*written = text;
}
// config.toml changed on disk: a hand edit applies, the shell's own saves are skipped. whether it applied
pub fn reread() -> bool {
let mut written = written();
let text = fs::read_to_string(file()).unwrap_or_default();
if text == *written {
return false;
}
apply_text(&text);
*written = text;
true
}
// shell.conf's four settings, moved to their places in config.toml
@@ -667,26 +698,42 @@ pub fn override_of(path: &str) -> Option<Value> {
pub fn set(path: &str, value: Value) -> Result<(), String> {
check(path, &value)?;
let mut written = written();
VALUES.write().unwrap_or_else(PoisonError::into_inner).insert(path.to_string(), value);
save()
save(&mut written)
}
// back to the default (a Hyprland option: back to Hyprland's own config)
pub fn reset(path: &str) -> Result<(), String> {
let mut written = written();
VALUES.write().unwrap_or_else(PoisonError::into_inner).remove(path);
save()
save(&mut written)
}
fn save() -> Result<(), String> {
fs::write(file(), render()).map_err(|error| format!("couldn't write {}: {error}", file().display()))?;
fn save(written: &mut String) -> Result<(), String> {
let text = render();
write_file(&text).map_err(|error| format!("couldn't write {}: {error}", file().display()))?;
*written = text;
theme::touch();
Ok(())
}
// whole or not at all: a new file renamed over the old, so no reader sees half of it
fn write_file(text: &str) -> std::io::Result<()> {
let fresh = file().with_extension("toml.new");
fs::write(&fresh, text)?;
fs::rename(&fresh, file())
}
// config.toml as the shell writes it: every setting with its help, then the Hyprland options set here
pub fn render() -> String {
let mut out = String::from(