Skip to content
Open
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
28 changes: 19 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,22 +127,32 @@ impl Config {
}

pub fn save_to(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut temporary = path.as_os_str().to_os_string();
temporary.push(format!(".{}.tmp", std::process::id()));
let temporary = PathBuf::from(temporary);
fs::write(&temporary, toml::to_string_pretty(self)?)?;
fs::rename(temporary, path)?;
Ok(())
paths::write_atomic(path, toml::to_string_pretty(self)?.as_bytes())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn saving_through_a_symlink_updates_the_link_target() {
let directory =
std::env::temp_dir().join(format!("whispers-config-symlink-{}", std::process::id()));
let _ = fs::remove_dir_all(&directory);
assert!(fs::create_dir_all(&directory).is_ok());
let target = directory.join("dotfiles-config.toml");
let link = directory.join("config.toml");
assert!(fs::write(&target, "version = 1\n").is_ok());
assert!(std::os::unix::fs::symlink(&target, &link).is_ok());

assert!(Config::default().save_to(&link).is_ok());

assert!(link.is_symlink());
assert!(fs::read_to_string(&target).is_ok_and(|text| text.contains("version = 2")));
let _ = fs::remove_dir_all(&directory);
}

#[test]
fn existing_unversioned_configuration_requires_migration() {
let path = std::env::temp_dir().join(format!(
Expand Down
33 changes: 32 additions & 1 deletion src/paths.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
use std::env;
use std::path::PathBuf;
use std::fs;
use std::path::{Path, PathBuf};

use crate::{Error, Result};

pub const MANAGED_LLAMA_RUNTIME: &str = "runtimes/llama.cpp/b10258-vulkan";

/// Writes `contents` to `path` through an atomic rename, following symbolic
/// links so that dotfile-managed files stay links to their real location.
pub(crate) fn write_atomic(path: &Path, contents: &[u8]) -> Result<()> {
let target = resolve_symlink(path);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
let mut temporary = target.as_os_str().to_os_string();
temporary.push(format!(".{}.tmp", std::process::id()));
let temporary = PathBuf::from(temporary);
fs::write(&temporary, contents)?;
fs::rename(&temporary, &target)?;
Ok(())
}

fn resolve_symlink(path: &Path) -> PathBuf {
if !path.is_symlink() {
return path.to_path_buf();
}
if let Ok(canonical) = fs::canonicalize(path) {
return canonical;
}
let parent = path.parent().unwrap_or_else(|| Path::new("."));
match fs::read_link(path) {
Ok(link) if link.is_absolute() => link,
Ok(link) => parent.join(link),
Err(_) => path.to_path_buf(),
}
}

pub fn config_dir() -> Result<PathBuf> {
xdg_path("XDG_CONFIG_HOME", ".config").map(|path| path.join("whispers"))
}
Expand Down
36 changes: 22 additions & 14 deletions src/vocabulary.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::path::Path;

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -57,13 +57,7 @@ impl Vocabulary {
}

pub fn save_to(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let temporary = temporary_path(path);
fs::write(&temporary, toml::to_string_pretty(self)?)?;
fs::rename(temporary, path)?;
Ok(())
paths::write_atomic(path, toml::to_string_pretty(self)?.as_bytes())
}

pub fn add(&mut self, term: &str, aliases: Vec<String>) -> Result<()> {
Expand Down Expand Up @@ -185,16 +179,30 @@ fn word_character(character: char) -> bool {
character.is_alphanumeric() || character == '_'
}

fn temporary_path(path: &Path) -> PathBuf {
let mut temporary = path.as_os_str().to_os_string();
temporary.push(format!(".{}.tmp", std::process::id()));
PathBuf::from(temporary)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn saving_through_a_symlink_updates_the_link_target() {
let directory = std::env::temp_dir().join(format!(
"whispers-vocabulary-symlink-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&directory);
assert!(fs::create_dir_all(&directory).is_ok());
let target = directory.join("dotfiles-vocabulary.toml");
let link = directory.join("vocabulary.toml");
assert!(fs::write(&target, "version = 0\n").is_ok());
assert!(std::os::unix::fs::symlink(&target, &link).is_ok());

assert!(Vocabulary::default().save_to(&link).is_ok());

assert!(link.is_symlink());
assert!(fs::read_to_string(&target).is_ok_and(|text| text.contains("version = 1")));
let _ = fs::remove_dir_all(&directory);
}

#[test]
fn replaces_aliases_at_word_boundaries() {
let vocabulary = Vocabulary {
Expand Down
Loading