Skip to content

Feat/respect xdg spec on all os #2627

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* add `use_selection_fg` to theme file to allow customizing selection foreground color [[@Upsylonbare](https://github.com/Upsylonbare)] ([#2515](https://github.com/gitui-org/gitui/pull/2515))

### Changed
* Respect `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` irrespective of OS [[@KlassyKat](https://github.com/KlassyKat)] ([#1498](https://github.com/gitui-org/gitui/issues/1498))
* improve error messages [[@acuteenvy](https://github.com/acuteenvy)] ([#2617](https://github.com/gitui-org/gitui/pull/2617))
* increase MSRV from 1.70 to 1.81 [[@naseschwarz](https://github.com/naseschwarz)] ([#2094](https://github.com/gitui-org/gitui/issues/2094))
* improve syntax highlighting file detection [[@acuteenvy](https://github.com/acuteenvy)] ([#2524](https://github.com/extrawurst/gitui/pull/2524))
Expand Down
15 changes: 10 additions & 5 deletions KEY_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ Create a `key_bindings.ron` file like this:
)
```

The config file format based on the [Ron file format](https://github.com/ron-rs/ron).
The key binding config file uses the [Ron file format](https://github.com/ron-rs/ron).
The location of the file depends on your OS:
* `$HOME/.config/gitui/key_bindings.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/key_bindings.ron` (linux using XDG)
* `$HOME/.config/gitui/key_bindings.ron` (linux)
* `%APPDATA%/gitui/key_bindings.ron` (Windows)
`gitui` will look for an existing `/gitui` in the following order:
* `$XDG_CONFIG_HOME/gitui/` (with `XDG_CONFIG_HOME` set)
* `$HOME/.config/gitui/`
* Default OS Location:
* `$HOME/Library/Application Support/` (mac)
* `$HOME/.config/gitui/` (linux)
* `%APPDATA%/gitui/` (Windows)

Key bindings are configured in `key_bindings.ron` within your first found `gitui` config folder.

See all possible keys to overwrite in gitui: [here](https://github.com/gitui-org/gitui/blob/master/src/keys/key_list.rs#L83)

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,9 @@ see [FAQs page](./FAQ.md)
To run with logging enabled run `gitui -l`.

This will log to:

- With `XDG_CACHE_HOME` set: `$XDG_CACHE_HOME/gitui/gitui.log`
or default to
- macOS: `$HOME/Library/Caches/gitui/gitui.log`
- Linux using `XDG`: `$XDG_CACHE_HOME/gitui/gitui.log`
- Linux: `$HOME/.cache/gitui/gitui.log`
- Windows: `%LOCALAPPDATA%/gitui/gitui.log`

Expand Down
21 changes: 13 additions & 8 deletions THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ default on light terminal:

To change the colors of the default theme you need to add a `theme.ron` file that contains the colors you want to override. Note that you don’t have to specify the full theme anymore (as of 0.23). Instead, it is sufficient to override just the values that you want to differ from their default values.

The file uses the [Ron format](https://github.com/ron-rs/ron) and is located at one of the following paths, depending on your operating system:

* `$HOME/.config/gitui/theme.ron` (mac)
* `$XDG_CONFIG_HOME/gitui/theme.ron` (linux using XDG)
* `$HOME/.config/gitui/theme.ron` (linux)
* `%APPDATA%/gitui/theme.ron` (Windows)

Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron`, this will load the theme in `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`.
The theme file uses the [Ron file format](https://github.com/ron-rs/ron).
The location of the file depends on your OS:
`gitui` will look for an existing `/gitui` in the following order:
* `$XDG_CONFIG_HOME/gitui/` (with `XDG_CONFIG_HOME` set)
* `$HOME/.config/gitui/`
* Default OS Location:
* `$HOME/Library/Application Support/` (mac)
* `$HOME/.config/gitui/` (linux)
* `%APPDATA%/gitui/` (Windows)

The theme is configured in `theme.ron` within your first found `gitui` config folder.

Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. Calling `gitui -t arc.ron` will load the `arc.ron` theme from your first found `/gitui` config folder using the logic above.

Example theme override:

Expand Down
115 changes: 98 additions & 17 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ pub fn process_cmdline() -> Result<CliArgs> {
.map_or_else(|| PathBuf::from("theme.ron"), PathBuf::from);

let confpath = get_app_config_path()?;
fs::create_dir_all(&confpath)?;
let theme = confpath.join(arg_theme);

let notify_watcher: bool =
Expand Down Expand Up @@ -149,28 +148,110 @@ fn setup_logging(path_override: Option<PathBuf>) -> Result<()> {
Ok(())
}

fn get_path_from_candidates(
candidates: impl IntoIterator<Item = Option<PathBuf>>,
) -> Result<PathBuf> {
let mut target_dir = None;

// Filter into existing directories
for potential_dir in
candidates.into_iter().flatten().filter(|p| p.is_dir())
Comment on lines +157 to +158
Copy link
Contributor

@naseschwarz naseschwarz Apr 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, one more remark: At least for XDG, Path::is_dir isn't a precondition for considering that path. If it doesn't exist, create it (with permissions 0700)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If, when attempting to write a file, the destination directory is non-existent an attempt should be made to create it with permission 0700.

This only applies when attempting to write a file.

When attempting to read a file, if for any reason a file in a certain directory is unaccessible, e.g. because the directory is non-existent, the file is non-existent or the user is not authorized to open the file, then the processing of the file in that directory should be skipped.

So believe this point is fairly moot when it comes to XDG_CONFIG_HOME.

That being said it does have some consideration for XDG_CACHE_HOME, but I think there is still some room for interpretation. I think you could consider that line to be referring to the creation of an app subdirectory and not to create the root directory itself. In which case that is already being done. Furthermore when it comes to creating good UX a user may have an XDG_CACHE_HOME defined which would result in that being the location to write to by default. But say the user wanted to keep XDG_CACHE_HOME only having the logs of particular apps creating a gitui folder in the fallback cache directory would allow them to filter it out.

I don't really imagine anyone wanting to do that, but it seems superior to have the option than not to.

Copy link
Contributor

@naseschwarz naseschwarz May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for XDG_CONFIG_HOME it does not make a difference as of now, as we don't create things there. For XDG_CACHE_HOME it does, though.

It worked before and does not work now. I'm sorry to say, but I don't think I'd want to approve this. If this were to fix a huge issue, okay, but this only addresses a (very) peculiar concern on windows.

{
let search_path = potential_dir.join("gitui");

// Prefer preexisting gitui directory
if search_path.is_dir() {
target_dir = Some(search_path);
break;
}

// Fallback to first existing directory
target_dir.get_or_insert(search_path);
}

target_dir.ok_or_else(|| {
anyhow!("failed to find valid path within candidates")
})
}

// "If an implementation encounters a relative path in any of these variables it should consider the path invalid and ignore it."
fn get_xdg_path(xdg_var: &str) -> Option<PathBuf> {
env::var_os(xdg_var)
.filter(|var| !var.is_empty())
.map(PathBuf::from)
.filter(|p| p.is_absolute())
}

fn get_app_cache_path() -> Result<PathBuf> {
let mut path = dirs::cache_dir()
.ok_or_else(|| anyhow!("failed to find os cache dir."))?;
let cache_dir_candidates =
[get_xdg_path("XDG_CACHE_HOME"), dirs::cache_dir()];

let cache_dir = get_path_from_candidates(cache_dir_candidates)
.map_err(|_| anyhow!("failed to find valid cache dir."))?;

path.push("gitui");
fs::create_dir_all(&path)?;
Ok(path)
fs::create_dir_all(&cache_dir)?;
Ok(cache_dir)
}

pub fn get_app_config_path() -> Result<PathBuf> {
let mut path = if cfg!(target_os = "macos") {
dirs::home_dir().map(|h| h.join(".config"))
} else {
dirs::config_dir()
}
.ok_or_else(|| anyhow!("failed to find os config dir."))?;
// List of potential config directories in order of priority
let config_dir_candidates = [
get_xdg_path("XDG_CONFIG_HOME"),
// This is in the list since it was the hardcoded behavior on macos before
// I expect this to be what most people have XDG_CONFIG_HOME set to already
// But explicitly including this will avoid breaking anyone's existing config
dirs::home_dir().map(|p| p.join(".config")),
dirs::config_dir(),
];

path.push("gitui");
Ok(path)
get_path_from_candidates(config_dir_candidates)
.map_err(|_| anyhow!("failed to find valid config dir."))
}

#[test]
fn verify_app() {
app().debug_assert();
#[cfg(test)]
mod tests {
use std::fs;

use super::{app, get_path_from_candidates};
use tempfile::tempdir;

#[test]
fn verify_app() {
app().debug_assert();
}

#[test]
fn test_config_dir_candidates_from_preexisting() {
let temp_dummy_1 = tempdir().expect("should create temp dir");
let temp_dummy_2 = tempdir().expect("should create temp dir");
let temp_target = tempdir().expect("should create temp dir");
let temp_goal = temp_target.path().join("gitui");

fs::create_dir_all(&temp_goal)
.expect("should create temp target directory");

let candidates = [
Some(temp_dummy_1.path().to_path_buf()),
Some(temp_target.path().to_path_buf()),
Some(temp_dummy_2.path().to_path_buf()),
];
let result = get_path_from_candidates(candidates)
.expect("should find the included target");
assert_eq!(result, temp_goal);
}

#[test]
fn test_config_dir_candidates_no_preexisting() {
let temp_dummy_1 = tempdir().expect("should create temp dir");
let temp_dummy_2 = tempdir().expect("should create temp dir");

let candidates = [
Some(temp_dummy_1.path().to_path_buf()),
Some(temp_dummy_2.path().to_path_buf()),
];

let result = get_path_from_candidates(candidates)
.expect("should return first candidate");
assert_eq!(result, temp_dummy_1.path().join("gitui"));
}
}