Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ body:
attributes:
label: Logs
description: |
Relevant lines from `colony 2>&1` or `~/.cache/colony/`. Redact any
Relevant lines from `colony 2>&1` or `~/.local/share/Colony/Colony/colony.log`. Redact any
personal paths or tokens before pasting.
render: shell

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ See [docs/architecture.md](docs/architecture.md) for the full layout. Short vers

### Reporting bugs / feature requests

Use the templates in `.github/ISSUE_TEMPLATE/` when filing. Include Colony version (`colony --version`), OS, and relevant log excerpts from `~/.cache/colony/colony.log`.
Use the templates in `.github/ISSUE_TEMPLATE/` when filing. Include Colony version (`colony --version`), OS, and relevant log excerpts from `~/.local/share/Colony/Colony/colony.log`.

---

Expand Down
9 changes: 2 additions & 7 deletions config/colony.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[scan]
# Dossiers scannés sous Windows (variables d'environnement supportées)
# Directories scanned on Windows (environment variables are expanded)
windows = [
"${ProgramData}\\Microsoft\\Windows\\Start Menu\\Programs",
"${APPDATA}\\Microsoft\\Windows\\Start Menu\\Programs",
]

# Dossiers scannés sous Linux/macOS (variables d'environnement supportées)
# Directories scanned on Linux/macOS (environment variables are expanded)
unix = [
"${HOME}/.local/share/applications",
"/usr/share/applications",
Expand All @@ -14,8 +14,3 @@ unix = [
"${HOME}/.local/share/flatpak/exports/share/applications",
"/var/lib/snapd/desktop/applications",
]

# Dossiers qui contiennent uniquement des applications Colony (pour classer l'origine)
colony = [
"${HOME}/.local/share/colony/applications",
]
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ All async operations (API calls, downloads, scanning) return a `Task<Message>` t
| Preferences | `~/.config/Colony/Colony/preferences/preferences.json` | Permanent |
| Favorites | `~/.config/Colony/Colony/preferences/favorites.json` | Permanent |
| OAuth token | OS Keychain / `~/.config/Colony/Colony/auth/github_token.json` | Permanent |
| Diagnostics log | `~/.cache/Colony/Colony/colony.log` | Truncated per run |
| Diagnostics log | `~/.local/share/Colony/Colony/colony.log` | Truncated per run |
| Installed versions | `~/.local/share/Colony/apps/<repo>/.colony_version` | Permanent |
| Resolved asset | `~/.local/share/Colony/apps/<repo>/.colony_asset` | Permanent |
| Colony binaries | `~/.local/share/Colony/apps/<repo>/` | Permanent |
| Self-update staging | `~/.cache/Colony/Colony/update-staging/` | Temporary |
| Self-update staging | `~/.cache/Colony/Colony/update-staging/` | Emptied at every start |

## GitHub API

Expand Down
7 changes: 5 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ If your app is installed but not in any of these (portable binary without `.desk
| System app scan cache | `~/.cache/Colony/Colony/scan_cache.json` |
| Self-update staging | `~/.cache/Colony/Colony/update-staging/` |
| HTTP ETag cache | `~/.cache/Colony/Colony/http_etags.json` |
| Diagnostics log | `~/.cache/Colony/Colony/colony.log` |
| Diagnostics log | `~/.local/share/Colony/Colony/colony.log` |

On Windows all three roots are `%LOCALAPPDATA%\Colony\`, and on macOS config
and data share `~/Library/Application Support/Colony/` while the cache sits in
Expand Down Expand Up @@ -162,7 +162,10 @@ Themes apply immediately, no restart. If a palette looks off, verify Settings
Yes. Colony looks for a `categories.json` override before falling back to the
copy compiled into the binary, in this order:

1. `~/.config/Colony/Colony/categories.json`
1. `~/.config/colony/categories.json` on Linux,
`~/Library/Application Support/colony/categories.json` on macOS, and
`%LOCALAPPDATA%\colony\categories.json` on Windows, where
`%APPDATA%\colony\` is still read after it
2. next to the `colony` binary
3. `./config/categories.json`

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,5 @@ upgraded alongside the rest of your system.
[colony-spec.md](colony-spec.md) for the full manifest reference.
- **Rate limit hit?** Connect a GitHub account (section 6).
- **Download fails?** Check your network. Colony retries up to three times and resumes where it stopped; logs
go to `~/.cache/Colony/Colony/colony.log` if you need to dig in.
go to `~/.local/share/Colony/Colony/colony.log` if you need to dig in.
- **More questions?** See the [FAQ](faq.md).
55 changes: 47 additions & 8 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ use std::path::PathBuf;
/// Resolve an external config file named `file_name` (e.g. `"categories.json"`).
///
/// Search order, first existing file wins:
/// 1. `~/.config/colony/<file_name>` — per-user override (XDG config dir).
/// 2. `<exe_dir>/config/<file_name>` — config shipped next to the binary.
/// 3. `./config/<file_name>` — current working dir (dev: `cargo run`).
/// 1. `<config_local>/colony/<file_name>`: per-user override, `~/.config/colony/`
/// on Linux, `~/Library/Application Support/colony/` on macOS and
/// `%LOCALAPPDATA%\colony\` on Windows.
/// 2. `<config>/colony/<file_name>`: Windows only, the Roaming folder that
/// earlier versions read, so an override placed there keeps working.
/// 3. `<exe_dir>/config/<file_name>`: config shipped next to the binary.
/// 4. `./config/<file_name>`: current working dir (dev: `cargo run`).
///
/// Returns `None` when no candidate exists; callers should then use their embedded
/// or built-in defaults.
Expand All @@ -26,19 +30,27 @@ pub fn resolve_config_path(file_name: &str) -> Option<PathBuf> {
fn candidate_paths(file_name: &str) -> Vec<PathBuf> {
let mut candidates = Vec::new();

// 1. ~/.config/colony/<file_name>
if let Some(config_dir) = dirs::config_dir() {
candidates.push(config_dir.join("colony").join(file_name));
// 1. Local, never Roaming: the shared layout keeps Colony out of Roaming.
// 2. Roaming, only where it differs (Windows), for overrides placed there
// before. On Linux and macOS the two resolve to the same directory.
for root in [dirs::config_local_dir(), dirs::config_dir()]
.into_iter()
.flatten()
{
let path = root.join("colony").join(file_name);
if !candidates.contains(&path) {
candidates.push(path);
}
}

// 2. <exe_dir>/config/<file_name>
// 3. <exe_dir>/config/<file_name>
if let Ok(exe) = std::env::current_exe() {
if let Some(exe_dir) = exe.parent() {
candidates.push(exe_dir.join("config").join(file_name));
}
}

// 3. ./config/<file_name> (dev convenience)
// 4. ./config/<file_name> (dev convenience)
candidates.push(PathBuf::from("config").join(file_name));

candidates
Expand All @@ -64,6 +76,33 @@ mod tests {
);
}

#[test]
fn the_local_override_wins_over_roaming() {
// Both sides read XDG_CONFIG_HOME, which other tests redirect.
let _guard = crate::persistence::XDG_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let candidates = candidate_paths("colony.toml");
let local = dirs::config_local_dir()
.expect("config_local_dir")
.join("colony")
.join("colony.toml");
let roaming = dirs::config_dir()
.expect("config_dir")
.join("colony")
.join("colony.toml");

assert_eq!(candidates[0], local, "Local is searched first");
// Roaming is still read, right after Local, and listed once: on Linux
// and macOS it is the same path as Local.
let at = candidates.iter().position(|p| p == &roaming);
assert!(matches!(at, Some(0) | Some(1)), "{candidates:?}");
assert_eq!(candidates.iter().filter(|p| *p == &roaming).count(), 1);
if local != roaming {
assert_eq!(at, Some(1));
}
}

#[test]
fn first_existing_returns_first_present_file() {
let dir = std::env::temp_dir();
Expand Down
77 changes: 56 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,33 @@ mod update;

use state::{default_font, App};

/// Where diagnostics land. A plain truncate-on-start file, not a rolling
/// appender: one run's worth of log is what a bug report needs, and it keeps
/// the dependency set unchanged.
fn log_file_path() -> Option<std::path::PathBuf> {
// The shared layout, same as every other cache: <cache>/Colony/Colony/.
colony_ui::paths::cache_dir("Colony")
.ok()
.map(|dir| dir.join("colony.log"))
/// Run `f` with its log lines held in memory, and return them.
///
/// The startup migration must run before anything creates a directory, the
/// log file included, yet what it reports belongs in that file.
fn capture_log(f: impl FnOnce()) -> Vec<u8> {
#[derive(Clone, Default)]
struct Buffer(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for Buffer {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
let mut buffer = self.0.lock().unwrap_or_else(|e| e.into_inner());
buffer.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

let buffer = Buffer::default();
let writer = buffer.clone();
let subscriber = tracing_subscriber::fmt()
.with_writer(move || writer.clone())
.with_ansi(false)
.finish();
tracing::subscriber::with_default(subscriber, f);
let mut lines = buffer.0.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *lines)
}

/// Answer `--version` / `--help` without opening a window. The bug template
Expand Down Expand Up @@ -75,9 +94,11 @@ fn handle_cli_flags() -> bool {
Diagnostics are written to {} and to stderr.\n\
Set RUST_LOG=debug for more detail.",
env!("CARGO_PKG_VERSION"),
log_file_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "(no cache directory)".into()),
// `locate`, not the creating helper: printing help must not
// bring a directory into existence before the migration runs.
colony_ui::paths::locate::data_dir("Colony")
.map(|dir| dir.join("colony.log").display().to_string())
.unwrap_or_else(|_| "(no data directory)".into()),
);
true
}
Expand Down Expand Up @@ -190,6 +211,13 @@ pub fn main() -> iced::Result {
return Ok(());
}

// Earlier versions wrote to different directories; move them before
// anything creates a path. That includes the log file: on Windows its data
// directory is the very config directory the first move targets, and a
// move skips a target that already exists. No-op once done, and on a fresh
// install.
let migration_log = capture_log(crate::persistence::migrate_legacy_paths);

// `EnvFilter::from_default_env().add_directive(INFO)` looked like "default
// to info", but a bare `RUST_LOG=debug` parses to a directive that compares
// Equal to the added one, so add_directive REPLACED it and the user's
Expand All @@ -206,20 +234,27 @@ pub fn main() -> iced::Result {
// Log to a file as well as stderr: a .desktop launch has no terminal, and
// on Windows there is no console at all, so stderr-only meant that every
// warning in the codebase reached nobody in any shipped configuration.
// A plain truncate-on-start file in the data directory, not a rolling
// appender: one run's worth of log is what a bug report needs, and it keeps
// the dependency set unchanged.
use std::io::Write as _;
use tracing_subscriber::fmt::writer::MakeWriterExt;
match log_file_path().and_then(|p| std::fs::File::create(p).ok()) {
Some(file) => tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::sync::Mutex::new(file).and(std::io::stderr))
.with_ansi(false)
.init(),
let _ = std::io::stderr().write_all(&migration_log);
let log_file = crate::persistence::colony_log_path()
.ok()
.and_then(|path| std::fs::File::create(path).ok());
match log_file {
Some(mut file) => {
let _ = file.write_all(&migration_log);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::sync::Mutex::new(file).and(std::io::stderr))
.with_ansi(false)
.init()
}
None => tracing_subscriber::fmt().with_env_filter(filter).init(),
}

// Earlier versions wrote to different directories; move them before
// anything reads a path. No-op once done, and on a fresh install.
crate::persistence::migrate_legacy_paths();

// Honor the saved language preference over environment locale detection,
// and reopen at the last persisted window size (clamped to sanity).
let prefs = crate::persistence::load_preferences();
Expand Down
Loading
Loading