Skip to main content

rvlib/
cfg.rs

1use crate::{
2    cache::FileCacheCfgArgs,
3    file_util::{
4        self, DEFAULT_PRJ_PATH, DEFAULT_TMPDIR, copy_and_unzip, dl_and_unzip, path_to_str,
5    },
6    result::trace_ok_err,
7    sort_params::SortParams,
8    ssh,
9};
10use rvimage_domain::{RvResult, rverr, to_rv};
11use serde::{Deserialize, Serialize, de::DeserializeOwned};
12use std::{
13    fmt::Debug,
14    fs,
15    path::{Path, PathBuf},
16};
17use tracing::{info, warn};
18
19#[cfg(feature = "azure_blob")]
20#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
21pub struct AzureBlobCfgLegacy {
22    pub connection_string_path: String,
23    pub container_name: String,
24    pub prefix: String,
25}
26#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
27pub struct SshCfgLegacy {
28    pub user: String,
29    pub ssh_identity_file_path: String,
30    n_reconnection_attempts: Option<usize>,
31    pub remote_folder_paths: Vec<String>,
32    pub address: String,
33}
34#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
35pub struct CfgLegacy {
36    pub connection: Connection,
37    pub cache: Cache,
38    http_address: Option<String>,
39    tmpdir: Option<String>,
40    current_prj_path: Option<PathBuf>,
41    pub file_cache_args: Option<FileCacheCfgArgs>,
42    pub ssh_cfg: SshCfgLegacy,
43    pub home_folder: Option<String>,
44    pub py_http_reader_cfg: Option<PyHttpReaderCfg>,
45    pub darkmode: Option<bool>,
46    pub n_autosaves: Option<u8>,
47    pub import_old_path: Option<String>,
48    pub import_new_path: Option<String>,
49    #[cfg(feature = "azure_blob")]
50    pub azure_blob_cfg: Option<AzureBlobCfgLegacy>,
51}
52impl CfgLegacy {
53    pub fn to_cfg(self) -> Cfg {
54        let usr = CfgUsr {
55            darkmode: self.darkmode,
56            n_autosaves: self.n_autosaves,
57            home_folder: self.home_folder,
58            cache: self.cache,
59            tmpdir: self.tmpdir,
60            current_prj_path: self.current_prj_path,
61            file_cache_args: self.file_cache_args.unwrap_or_default(),
62            image_change_delay_on_held_key_ms: get_image_change_delay_on_held_key_ms(),
63
64            ssh: SshCfgUsr {
65                user: self.ssh_cfg.user,
66                ssh_identity_file_path: self.ssh_cfg.ssh_identity_file_path,
67                n_reconnection_attempts: self.ssh_cfg.n_reconnection_attempts,
68            },
69            n_prev_thumbs: get_default_n_thumbs(),
70            n_next_thumbs: get_default_n_thumbs(),
71            thumb_w_max: get_default_thumb_w_max(),
72            thumb_h_max: get_default_thumb_h_max(),
73            hide_thumbs: true,
74            thumb_attrs_view: false,
75            azure_blob: None,
76            wand_many_headers: None,
77        };
78        let prj = CfgPrj {
79            connection: self.connection,
80            http_address: self.http_address,
81            py_http_reader_cfg: self.py_http_reader_cfg,
82            ssh: SshCfgPrj {
83                remote_folder_paths: self.ssh_cfg.remote_folder_paths,
84                address: self.ssh_cfg.address,
85            },
86            azure_blob: self.azure_blob_cfg.map(|ab| AzureBlobCfgPrj {
87                connection_string_path: ab.connection_string_path,
88                container_name: ab.container_name,
89                prefix: ab.prefix,
90                blob_list_timeout_s: get_blob_list_timeout_s(),
91            }),
92            sort_params: SortParams::default(),
93            wand_server: WandServerCfg::default(),
94            wand_many: WandManyCfg::default(),
95        };
96        Cfg { usr, prj }
97    }
98}
99
100pub fn get_cfg_path_legacy(homefolder: &Path) -> PathBuf {
101    homefolder.join("rv_cfg.toml")
102}
103
104pub fn get_cfg_path_usr(homefolder: &Path) -> PathBuf {
105    homefolder.join("rv_cfg_usr.toml")
106}
107
108pub fn get_cfg_path_prj(homefolder: &Path) -> PathBuf {
109    homefolder.join("rv_cfg_prjtmp.toml")
110}
111
112pub fn get_cfg_tmppath(cfg: &Cfg) -> PathBuf {
113    Path::new(cfg.tmpdir())
114        .join(".rvimage")
115        .join("rv_cfg_tmp.toml")
116}
117
118pub fn get_log_folder(homefolder: &Path) -> PathBuf {
119    homefolder.join("logs")
120}
121
122fn parse_toml_str<CFG: Debug + DeserializeOwned + Default>(toml_str: &str) -> RvResult<CFG> {
123    match toml::from_str(toml_str) {
124        Ok(cfg) => Ok(cfg),
125        Err(_) => {
126            // lets try replacing \ by / and see if we can parse it
127            let toml_str = toml_str.replace('\\', "/");
128            match toml::from_str(&toml_str) {
129                Ok(cfg) => Ok(cfg),
130                Err(_) => {
131                    // lets try replacing " by ' and see if we can parse it
132                    let toml_str = toml_str.replace('"', "'");
133                    toml::from_str(&toml_str)
134                        .map_err(|e| rverr!("failed to parse cfg due to {e:?}"))
135                }
136            }
137        }
138    }
139}
140
141pub fn read_cfg_gen<CFG: Debug + DeserializeOwned + Default>(
142    cfg_toml_path: &Path,
143) -> RvResult<CFG> {
144    if cfg_toml_path.exists() {
145        let toml_str = file_util::read_to_string(cfg_toml_path)?;
146        parse_toml_str(&toml_str)
147    } else {
148        warn!("cfg {cfg_toml_path:?} file does not exist. using default cfg");
149        Ok(CFG::default())
150    }
151}
152
153fn read_cfg_from_paths(
154    cfg_toml_path_usr: &Path,
155    cfg_toml_path_prj: &Path,
156    cfg_toml_path_legacy: &Path,
157) -> RvResult<Cfg> {
158    if cfg_toml_path_usr.exists() || cfg_toml_path_prj.exists() {
159        let usr = read_cfg_gen::<CfgUsr>(cfg_toml_path_usr)?;
160        let prj = read_cfg_gen::<CfgPrj>(cfg_toml_path_prj)?;
161        Ok(Cfg { usr, prj })
162    } else if cfg_toml_path_legacy.exists() {
163        tracing::warn!("using legacy cfg file {cfg_toml_path_legacy:?}");
164        let legacy = read_cfg_gen::<CfgLegacy>(cfg_toml_path_legacy)?;
165        Ok(legacy.to_cfg())
166    } else {
167        tracing::info!("no cfg file found. using default cfg");
168        Ok(Cfg::default())
169    }
170}
171
172pub fn write_cfg_str(cfg_str: &str, p: &Path, log: bool) -> RvResult<()> {
173    file_util::write(p, cfg_str)?;
174    if log {
175        info!("wrote cfg to {p:?}");
176    }
177    Ok(())
178}
179
180#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Copy, Default)]
181pub enum Connection {
182    Ssh,
183    PyHttp,
184    #[cfg(feature = "azure_blob")]
185    AzureBlob,
186    #[default]
187    Local,
188}
189#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone, Default)]
190pub enum Cache {
191    #[default]
192    FileCache,
193    NoCache,
194}
195#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
196pub struct SshCfgUsr {
197    pub user: String,
198    pub ssh_identity_file_path: String,
199    n_reconnection_attempts: Option<usize>,
200}
201#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
202pub struct SshCfgPrj {
203    pub remote_folder_paths: Vec<String>,
204    pub address: String,
205}
206#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
207pub struct SshCfg {
208    pub usr: SshCfgUsr,
209    pub prj: SshCfgPrj,
210}
211impl SshCfg {
212    pub fn n_reconnection_attempts(&self) -> usize {
213        let default = 5;
214        self.usr.n_reconnection_attempts.unwrap_or(default)
215    }
216}
217
218fn get_blob_list_timeout_s() -> u64 {
219    10
220}
221
222#[cfg(feature = "azure_blob")]
223#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
224pub struct AzureBlobCfgPrj {
225    #[serde(default)]
226    pub connection_string_path: String,
227    pub container_name: String,
228    pub prefix: String,
229    #[serde(default = "get_blob_list_timeout_s")]
230    pub blob_list_timeout_s: u64,
231}
232
233#[cfg(feature = "azure_blob")]
234impl Default for AzureBlobCfgPrj {
235    fn default() -> Self {
236        Self {
237            connection_string_path: "".to_string(),
238            container_name: "".to_string(),
239            prefix: "".to_string(),
240            blob_list_timeout_s: get_blob_list_timeout_s(),
241        }
242    }
243}
244
245#[cfg(feature = "azure_blob")]
246#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
247pub struct AzureBlobCfgUsr {
248    #[serde(default)]
249    pub connection_string: String,
250}
251
252#[derive(Deserialize, Serialize, PartialEq, Eq, Debug, Clone)]
253pub enum CmdServerSrc {
254    LocalZip(String),
255    LocalFolder(String),
256    UrlZip(String),
257    Gitrepo(String),
258}
259impl Default for CmdServerSrc {
260    fn default() -> Self {
261        CmdServerSrc::Gitrepo("".into())
262    }
263}
264
265impl CmdServerSrc {
266    pub fn put_to_dst(&self, prj_path: &Path, dst_folder: &Path) -> RvResult<()> {
267        match self {
268            CmdServerSrc::LocalZip(zip_path) => {
269                let zip_path = file_util::relative_to_prj_path(prj_path, zip_path)?;
270                copy_and_unzip(zip_path.as_path(), dst_folder)
271            }
272            CmdServerSrc::UrlZip(url) => dl_and_unzip(url, dst_folder),
273            CmdServerSrc::LocalFolder(folder_path) => {
274                let folder_path = file_util::relative_to_prj_path(prj_path, folder_path)?;
275                std::fs::create_dir_all(dst_folder).map_err(to_rv)?;
276                file_util::copy_folder_recursively(folder_path.as_path(), dst_folder)
277                    .map_err(to_rv)?;
278                Ok(())
279            }
280            CmdServerSrc::Gitrepo(repo) => {
281                let repo = if file_util::is_url(repo) {
282                    repo.to_string()
283                } else {
284                    let repo_path = file_util::relative_to_prj_path(prj_path, repo)?;
285                    path_to_str(&repo_path)?.to_string()
286                };
287                let repo = repo.replace('\\', "/");
288                let repo_name = repo
289                    .rsplit('/')
290                    .next()
291                    .unwrap_or(&repo)
292                    .trim_end_matches(".git");
293                let dst_folder = dst_folder.join(repo_name);
294
295                // Ensure parent directory exists, but not the clone destination
296                if let Some(parent) = dst_folder.parent() {
297                    fs::create_dir_all(parent).map_err(to_rv)?;
298                }
299
300                // gix::prepare_clone expects the destination to not exist
301                if dst_folder.exists() {
302                    fs::remove_dir_all(&dst_folder).map_err(to_rv)?;
303                }
304
305                tracing::info!("cloning git repo {repo} to {dst_folder:?}...");
306                let mut prepare = gix::prepare_clone(repo, &dst_folder).map_err(to_rv)?;
307                let (mut checkout, _) = prepare
308                    .fetch_then_checkout(
309                        gix::progress::Discard,
310                        &std::sync::atomic::AtomicBool::new(false),
311                    )
312                    .map_err(to_rv)?;
313                checkout
314                    .main_worktree(
315                        gix::progress::Discard,
316                        &std::sync::atomic::AtomicBool::new(false),
317                    )
318                    .map_err(to_rv)?;
319                Ok(())
320            }
321        }
322    }
323    pub fn relative_working_dir(&self) -> &str {
324        match self {
325            CmdServerSrc::LocalZip(zip_path) => zip_path
326                .trim_end_matches(".zip")
327                .rsplit('/')
328                .next()
329                .unwrap_or(""),
330            CmdServerSrc::UrlZip(url) => url
331                .trim_end_matches(".zip")
332                .rsplit('/')
333                .next()
334                .unwrap_or(""),
335            CmdServerSrc::Gitrepo(rep) => rep
336                .rsplit('/')
337                .next()
338                .unwrap_or("")
339                .trim_end_matches(".git"),
340            CmdServerSrc::LocalFolder(folder_path) => folder_path.rsplit('/').next().unwrap_or(""),
341        }
342    }
343}
344
345fn get_default_on_install_uv() -> bool {
346    true
347}
348
349#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
350pub struct WandServerCfg {
351    pub src: CmdServerSrc,
352    pub additional_files: Vec<String>,
353    pub setup_cmd: String,
354    pub setup_args: Vec<String>,
355    pub local_folder: Option<String>,
356    #[serde(default = "get_default_on_install_uv")]
357    pub install_uv: bool,
358}
359
360fn get_wandmany_default_timeout() -> usize {
361    600
362}
363
364/// In contrast to annotations of the currently opened image in the tool's predictive labelling setting,
365/// this wand-cfg is about annotating the whole project.
366#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
367pub struct WandManyCfg {
368    pub url: String,
369    #[serde(default = "get_wandmany_default_timeout")]
370    pub timeout_s: usize,
371    pub prj_name: String,
372}
373
374impl Default for WandManyCfg {
375    fn default() -> Self {
376        Self {
377            url: "".into(),
378            timeout_s: get_wandmany_default_timeout(),
379            prj_name: "".to_string(),
380        }
381    }
382}
383
384#[cfg(feature = "azure_blob")]
385#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
386pub struct AzureBlobCfg {
387    pub prj: AzureBlobCfgPrj,
388    pub usr: AzureBlobCfgUsr,
389}
390
391#[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Eq)]
392pub struct PyHttpReaderCfg {
393    pub server_addresses: Vec<String>,
394}
395
396#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
397pub enum ExportPathConnection {
398    Ssh,
399    #[default]
400    Local,
401}
402impl ExportPathConnection {
403    pub fn write_bytes(
404        &self,
405        data: &[u8],
406        dst_path: &Path,
407        ssh_cfg: Option<&SshCfg>,
408    ) -> RvResult<()> {
409        match (self, ssh_cfg) {
410            (ExportPathConnection::Ssh, Some(ssh_cfg)) => {
411                let sess = ssh::auth(ssh_cfg)?;
412                ssh::write_bytes(data, dst_path, &sess).map_err(to_rv)?;
413                Ok(())
414            }
415            (ExportPathConnection::Local, _) => {
416                file_util::write(dst_path, data)?;
417                Ok(())
418            }
419            (ExportPathConnection::Ssh, None) => Err(rverr!("cannot save to ssh. config missing")),
420        }
421    }
422    pub fn write(&self, data_str: &str, dst_path: &Path, ssh_cfg: Option<&SshCfg>) -> RvResult<()> {
423        self.write_bytes(data_str.as_bytes(), dst_path, ssh_cfg)
424    }
425    pub fn read(&self, src_path: &Path, ssh_cfg: Option<&SshCfg>) -> RvResult<String> {
426        match (self, ssh_cfg) {
427            (ExportPathConnection::Ssh, Some(ssh_cfg)) => {
428                let sess = ssh::auth(ssh_cfg)?;
429                let read_bytes = ssh::download(path_to_str(src_path)?, &sess)?;
430                String::from_utf8(read_bytes).map_err(to_rv)
431            }
432            (ExportPathConnection::Local, _) => file_util::read_to_string(src_path),
433            (ExportPathConnection::Ssh, None) => {
434                Err(rverr!("cannot read from ssh. config missing"))
435            }
436        }
437    }
438}
439#[derive(Deserialize, Serialize, Default, Clone, Debug, PartialEq, Eq)]
440pub struct ExportPath {
441    pub path: PathBuf,
442    pub conn: ExportPathConnection,
443}
444
445pub enum Style {
446    Dark,
447    Light,
448}
449
450fn get_default_n_thumbs() -> usize {
451    4
452}
453fn get_default_thumb_w_max() -> u32 {
454    200
455}
456fn get_default_thumb_h_max() -> u32 {
457    100
458}
459
460fn get_default_n_autosaves() -> Option<u8> {
461    Some(2)
462}
463
464fn get_image_change_delay_on_held_key_ms() -> u64 {
465    300
466}
467
468#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
469pub struct CfgUsr {
470    pub darkmode: Option<bool>,
471    #[serde(default = "get_default_n_autosaves")]
472    pub n_autosaves: Option<u8>,
473
474    #[serde(default = "get_image_change_delay_on_held_key_ms")]
475    pub image_change_delay_on_held_key_ms: u64,
476
477    // This is only variable to make the CLI and tests not override your config.
478    // You shall not change this when actually running RV Image.
479    pub home_folder: Option<String>,
480
481    pub cache: Cache,
482    tmpdir: Option<String>,
483    current_prj_path: Option<PathBuf>,
484    #[serde(default)]
485    pub file_cache_args: FileCacheCfgArgs,
486    pub ssh: SshCfgUsr,
487    #[serde(default = "get_default_n_thumbs")]
488    pub n_prev_thumbs: usize,
489    #[serde(default = "get_default_n_thumbs")]
490    pub n_next_thumbs: usize,
491    #[serde(default = "get_default_thumb_w_max")]
492    pub thumb_w_max: u32,
493    #[serde(default = "get_default_thumb_h_max")]
494    pub thumb_h_max: u32,
495    #[serde(default)]
496    pub hide_thumbs: bool,
497    #[serde(default)]
498    pub thumb_attrs_view: bool,
499    #[serde(default)]
500    pub azure_blob: Option<AzureBlobCfgUsr>,
501    #[serde(default)]
502    pub wand_many_headers: Option<String>,
503}
504
505impl CfgUsr {
506    pub fn get_n_autosaves(&self) -> u8 {
507        self.n_autosaves
508            .unwrap_or(get_default_n_autosaves().unwrap())
509    }
510    pub fn show_thumbs(&self) -> bool {
511        !self.hide_thumbs || self.thumb_attrs_view
512    }
513    pub fn show_main_image(&self) -> bool {
514        !self.thumb_attrs_view
515    }
516}
517
518#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
519pub struct CfgPrj {
520    pub py_http_reader_cfg: Option<PyHttpReaderCfg>,
521    pub connection: Connection,
522    http_address: Option<String>,
523    pub ssh: SshCfgPrj,
524    #[cfg(feature = "azure_blob")]
525    pub azure_blob: Option<AzureBlobCfgPrj>,
526    #[serde(default)]
527    pub sort_params: SortParams,
528    #[serde(default)]
529    pub wand_server: WandServerCfg,
530    #[serde(default)]
531    pub wand_many: WandManyCfg,
532}
533#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
534pub struct Cfg {
535    pub usr: CfgUsr,
536    pub prj: CfgPrj,
537}
538
539impl Cfg {
540    /// for multiple cli instances to run in parallel
541    pub fn with_unique_folders() -> Self {
542        let mut cfg = Self::default();
543        let uuid_str = format!("{}", uuid::Uuid::new_v4());
544        let tmpdir_str = DEFAULT_TMPDIR
545            .to_str()
546            .expect("default tmpdir does not exist. cannot work without")
547            .to_string();
548        cfg.usr.tmpdir = Some(format!("{tmpdir_str}/rvimage_tmp_{uuid_str}"));
549        let tmp_homedir = format!("{tmpdir_str}/rvimage_home_{uuid_str}");
550
551        // copy user cfg to tmp homedir
552        trace_ok_err(fs::create_dir_all(&tmp_homedir));
553        if let Some(home_folder) = &cfg.usr.home_folder {
554            let usrcfg_path = get_cfg_path_usr(Path::new(home_folder));
555            if usrcfg_path.exists()
556                && let Some(filename) = usrcfg_path.file_name()
557            {
558                trace_ok_err(fs::copy(
559                    &usrcfg_path,
560                    Path::new(&tmp_homedir).join(filename),
561                ));
562            }
563        }
564        cfg.usr.home_folder = Some(tmp_homedir);
565        cfg
566    }
567    pub fn ssh_cfg(&self) -> SshCfg {
568        SshCfg {
569            usr: self.usr.ssh.clone(),
570            prj: self.prj.ssh.clone(),
571        }
572    }
573    #[cfg(feature = "azure_blob")]
574    pub fn azure_blob_cfg(&self) -> Option<AzureBlobCfg> {
575        match (self.prj.azure_blob.as_ref(), self.usr.azure_blob.as_ref()) {
576            (Some(prj), Some(usr)) => Some(AzureBlobCfg {
577                prj: prj.clone(),
578                usr: usr.clone(),
579            }),
580            (Some(prj), None) => Some(AzureBlobCfg {
581                prj: prj.clone(),
582                usr: AzureBlobCfgUsr::default(),
583            }),
584            _ => None,
585        }
586    }
587    pub fn home_folder(&self) -> &str {
588        let ef = self.usr.home_folder.as_deref();
589        match ef {
590            None => file_util::get_default_homedir(),
591            Some(ef) => ef,
592        }
593    }
594
595    pub fn tmpdir(&self) -> &str {
596        match &self.usr.tmpdir {
597            Some(td) => td.as_str(),
598            None => DEFAULT_TMPDIR.to_str().unwrap(),
599        }
600    }
601
602    pub fn http_address(&self) -> &str {
603        match &self.prj.http_address {
604            Some(http_addr) => http_addr,
605            None => "127.0.0.1:5432",
606        }
607    }
608
609    pub fn current_prj_path(&self) -> &Path {
610        if let Some(pp) = &self.usr.current_prj_path {
611            pp
612        } else {
613            &DEFAULT_PRJ_PATH
614        }
615    }
616    pub fn set_current_prj_path(&mut self, pp: PathBuf) {
617        self.usr.current_prj_path = Some(pp);
618    }
619    pub fn unset_current_prj_path(&mut self) {
620        self.usr.current_prj_path = None;
621    }
622
623    pub fn write(&self) -> RvResult<()> {
624        let homefolder = Path::new(self.home_folder());
625        let cfg_usr_path = get_cfg_path_usr(homefolder);
626        if let Some(cfg_parent) = cfg_usr_path.parent() {
627            fs::create_dir_all(cfg_parent).map_err(to_rv)?;
628        }
629        let cfg_usr_str = toml::to_string_pretty(&self.usr).map_err(to_rv)?;
630        let log = true;
631        write_cfg_str(&cfg_usr_str, &cfg_usr_path, log).and_then(|_| {
632            let cfg_prj_path = get_cfg_path_prj(homefolder);
633            let cfg_prj_str = toml::to_string_pretty(&self.prj).map_err(to_rv)?;
634            write_cfg_str(&cfg_prj_str, &cfg_prj_path, log)
635        })
636    }
637    pub fn read(homefolder: &Path) -> RvResult<Self> {
638        let cfg_toml_path_usr = get_cfg_path_usr(homefolder);
639        let cfg_toml_path_prj = get_cfg_path_prj(homefolder);
640        let cfg_toml_path_legacy = get_cfg_path_legacy(homefolder);
641        read_cfg_from_paths(
642            &cfg_toml_path_usr,
643            &cfg_toml_path_prj,
644            &cfg_toml_path_legacy,
645        )
646    }
647}
648impl Default for Cfg {
649    fn default() -> Self {
650        let usr = CfgUsr::default();
651        let prj = CfgPrj::default();
652
653        let mut cfg = Cfg { usr, prj };
654        cfg.usr.current_prj_path = Some(DEFAULT_PRJ_PATH.to_path_buf());
655        cfg.usr.n_prev_thumbs = get_default_n_thumbs();
656        cfg.usr.n_next_thumbs = get_default_n_thumbs();
657        cfg.usr.thumb_w_max = get_default_thumb_w_max();
658        cfg.usr.thumb_h_max = get_default_thumb_h_max();
659        cfg.usr.hide_thumbs = true;
660        cfg
661    }
662}
663#[cfg(test)]
664use file_util::get_default_homedir;
665
666#[test]
667fn test_default_cfg_paths() {
668    get_default_homedir();
669    DEFAULT_PRJ_PATH.to_str().unwrap();
670    DEFAULT_TMPDIR.to_str().unwrap();
671}
672
673#[test]
674fn test_read_cfg_legacy() {
675    let test_folder = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test_data");
676    let cfg_toml_path_usr = test_folder.join("rv_cfg_usr_doesntexist.toml");
677    let cfg_toml_path_prj = test_folder.join("rv_cfg_prj_doesntexist.toml");
678    let cfg_toml_path_legacy = test_folder.join("rv_cfg_legacy.toml");
679    let cfg = read_cfg_from_paths(
680        &cfg_toml_path_usr,
681        &cfg_toml_path_prj,
682        &cfg_toml_path_legacy,
683    )
684    .unwrap();
685    assert_eq!(
686        cfg.usr.current_prj_path,
687        Some(PathBuf::from("/Users/ultrauser/Desktop/ultra.json"))
688    );
689    assert_eq!(cfg.usr.darkmode, Some(true));
690    assert_eq!(cfg.usr.ssh.user, "someuser");
691    assert_eq!(cfg.prj.ssh.address, "73.42.73.42")
692}
693
694#[cfg(test)]
695fn make_cfg_str(ssh_identity_filepath: &str) -> String {
696    let part1 = r#"
697[usr]
698n_autosaves = 10
699image_change_delay_on_held_key_ms = 10
700cache = "FileCache"
701current_prj_path = "someprjpath.json"
702
703[usr.file_cache_args]
704n_prev_images = 4
705n_next_images = 8
706n_threads = 2
707clear_on_close = true
708cachedir = "C:/Users/ShafeiB/.rvimage/cache"
709
710[usr.ssh]
711user = "auser"
712ssh_identity_file_path ="#;
713
714    let part2 = r#"
715[prj]
716connection = "Local"
717
718[prj.py_http_reader_cfg]
719server_addresses = [
720    "http://localhost:8000/somewhere",
721    "http://localhost:8000/elsewhere",
722]
723
724[prj.ssh]
725remote_folder_paths = ["/"]
726address = "12.11.10.13:22"
727
728[prj.sort_params]
729kind = "Natural"
730sort_by_filename = false
731"#;
732
733    format!("{part1} {ssh_identity_filepath} {part2}")
734}
735
736#[test]
737fn test_parse_toml() {
738    fn test(ssh_path: &str, ssh_path_expected: &str) {
739        let toml_str = make_cfg_str(ssh_path);
740        let cfg: Cfg = parse_toml_str(&toml_str).unwrap();
741        assert_eq!(cfg.usr.ssh.ssh_identity_file_path, ssh_path_expected);
742    }
743    test("\"c:\\somehome\\.ssh\\id_rsa\"", "c:/somehome/.ssh/id_rsa");
744    test(
745        "'c:\\some home\\.ssh\\id_rsa'",
746        "c:\\some home\\.ssh\\id_rsa",
747    );
748    test("\"/s omehome\\.ssh\\id_rsa\"", "/s omehome/.ssh/id_rsa");
749    test("'/some home/.ssh/id_rsa'", "/some home/.ssh/id_rsa");
750}