leta_config/
paths.rs

1use std::fs::File;
2use std::os::unix::io::AsRawFd;
3use std::path::PathBuf;
4
5pub fn get_config_dir() -> PathBuf {
6    let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
7    PathBuf::from(home).join(".config").join("leta")
8}
9
10pub fn get_config_path() -> PathBuf {
11    get_config_dir().join("config.toml")
12}
13
14pub fn get_cache_dir() -> PathBuf {
15    let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
16    PathBuf::from(home).join(".cache").join("leta")
17}
18
19pub fn get_log_dir() -> PathBuf {
20    get_cache_dir().join("log")
21}
22
23pub fn get_socket_path() -> PathBuf {
24    get_cache_dir().join("daemon.sock")
25}
26
27pub fn get_pid_path() -> PathBuf {
28    get_cache_dir().join("daemon.pid")
29}
30
31pub fn write_pid(path: &std::path::Path, pid: u32) -> std::io::Result<()> {
32    if let Some(parent) = path.parent() {
33        std::fs::create_dir_all(parent)?;
34    }
35    std::fs::write(path, pid.to_string())
36}
37
38pub fn remove_pid(path: &std::path::Path) {
39    let _ = std::fs::remove_file(path);
40}
41
42pub fn read_pid(path: &std::path::Path) -> Option<u32> {
43    std::fs::read_to_string(path)
44        .ok()
45        .and_then(|s| s.trim().parse().ok())
46}
47
48pub fn is_daemon_running() -> bool {
49    let pid_path = get_pid_path();
50    if let Some(pid) = read_pid(&pid_path) {
51        is_process_running(pid)
52    } else {
53        false
54    }
55}
56
57#[cfg(unix)]
58fn is_process_running(pid: u32) -> bool {
59    unsafe { libc::kill(pid as i32, 0) == 0 }
60}
61
62#[cfg(not(unix))]
63fn is_process_running(_pid: u32) -> bool {
64    false
65}
66
67pub fn detect_workspace_root(path: &std::path::Path) -> Option<PathBuf> {
68    let markers = [
69        ".git",
70        "pyproject.toml",
71        "setup.py",
72        "package.json",
73        "Cargo.toml",
74        "go.mod",
75        "pom.xml",
76        "build.gradle",
77        "Gemfile",
78        "composer.json",
79        "mix.exs",
80        "dune-project",
81    ];
82
83    let mut current = path.to_path_buf();
84    loop {
85        for marker in &markers {
86            if current.join(marker).exists() {
87                return Some(current);
88            }
89        }
90        if !current.pop() {
91            break;
92        }
93    }
94    None
95}
96
97pub struct DaemonLock {
98    _file: File,
99}
100
101impl DaemonLock {
102    pub fn acquire() -> Option<DaemonLock> {
103        let lock_path = get_pid_path().with_extension("lock");
104        if let Some(parent) = lock_path.parent() {
105            let _ = std::fs::create_dir_all(parent);
106        }
107
108        let file = match File::options()
109            .write(true)
110            .create(true)
111            .truncate(true)
112            .open(&lock_path)
113        {
114            Ok(f) => f,
115            Err(_) => return None,
116        };
117
118        let fd = file.as_raw_fd();
119        let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
120        if result == 0 {
121            Some(DaemonLock { _file: file })
122        } else {
123            None
124        }
125    }
126}