1use std::{
2 collections::HashMap, convert::TryFrom, fmt, io::prelude::*, path::PathBuf, str::FromStr,
3};
4
5use atuin_common::record::HostId;
6use clap::ValueEnum;
7use config::{
8 builder::DefaultState, Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat,
9};
10use eyre::{bail, eyre, Context, Error, Result};
11use fs_err::{create_dir_all, File};
12use humantime::parse_duration;
13use regex::RegexSet;
14use semver::Version;
15use serde::{Deserialize, Serialize};
16use serde_with::DeserializeFromStr;
17use time::{
18 format_description::{well_known::Rfc3339, FormatItem},
19 macros::format_description,
20 OffsetDateTime, UtcOffset,
21};
22use uuid::Uuid;
23
24pub const HISTORY_PAGE_SIZE: i64 = 100;
25pub const LAST_SYNC_FILENAME: &str = "last_sync_time";
26pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time";
27pub const LATEST_VERSION_FILENAME: &str = "latest_version";
28pub const HOST_ID_FILENAME: &str = "host_id";
29static EXAMPLE_CONFIG: &str = include_str!("../config.toml");
30
31mod dotfiles;
32
33#[derive(Clone, Debug, Deserialize, Copy, ValueEnum, PartialEq, Serialize)]
34pub enum SearchMode {
35 #[serde(rename = "prefix")]
36 Prefix,
37
38 #[serde(rename = "fulltext")]
39 #[clap(aliases = &["fulltext"])]
40 FullText,
41
42 #[serde(rename = "fuzzy")]
43 Fuzzy,
44
45 #[serde(rename = "skim")]
46 Skim,
47}
48
49impl SearchMode {
50 pub fn as_str(&self) -> &'static str {
51 match self {
52 SearchMode::Prefix => "PREFIX",
53 SearchMode::FullText => "FULLTXT",
54 SearchMode::Fuzzy => "FUZZY",
55 SearchMode::Skim => "SKIM",
56 }
57 }
58 pub fn next(&self, settings: &Settings) -> Self {
59 match self {
60 SearchMode::Prefix => SearchMode::FullText,
61 SearchMode::FullText if settings.search_mode == SearchMode::Skim => SearchMode::Skim,
63 SearchMode::FullText => SearchMode::Fuzzy,
65 SearchMode::Fuzzy | SearchMode::Skim => SearchMode::Prefix,
66 }
67 }
68}
69
70#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)]
71pub enum FilterMode {
72 #[serde(rename = "global")]
73 Global = 0,
74
75 #[serde(rename = "host")]
76 Host = 1,
77
78 #[serde(rename = "session")]
79 Session = 2,
80
81 #[serde(rename = "directory")]
82 Directory = 3,
83
84 #[serde(rename = "workspace")]
85 Workspace = 4,
86}
87
88impl FilterMode {
89 pub fn as_str(&self) -> &'static str {
90 match self {
91 FilterMode::Global => "GLOBAL",
92 FilterMode::Host => "HOST",
93 FilterMode::Session => "SESSION",
94 FilterMode::Directory => "DIRECTORY",
95 FilterMode::Workspace => "WORKSPACE",
96 }
97 }
98}
99
100#[derive(Clone, Debug, Deserialize, Copy, Serialize)]
101pub enum ExitMode {
102 #[serde(rename = "return-original")]
103 ReturnOriginal,
104
105 #[serde(rename = "return-query")]
106 ReturnQuery,
107}
108
109#[derive(Clone, Debug, Deserialize, Copy, Serialize)]
112pub enum Dialect {
113 #[serde(rename = "us")]
114 Us,
115
116 #[serde(rename = "uk")]
117 Uk,
118}
119
120impl From<Dialect> for interim::Dialect {
121 fn from(d: Dialect) -> interim::Dialect {
122 match d {
123 Dialect::Uk => interim::Dialect::Uk,
124 Dialect::Us => interim::Dialect::Us,
125 }
126 }
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq, DeserializeFromStr, Serialize)]
136pub struct Timezone(pub UtcOffset);
137impl fmt::Display for Timezone {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 self.0.fmt(f)
140 }
141}
142static OFFSET_FMT: &[FormatItem<'_>] =
144 format_description!("[offset_hour sign:mandatory padding:none][optional [:[offset_minute padding:none][optional [:[offset_second padding:none]]]]]");
145impl FromStr for Timezone {
146 type Err = Error;
147
148 fn from_str(s: &str) -> Result<Self> {
149 if matches!(s.to_lowercase().as_str(), "l" | "local") {
151 let offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
156 return Ok(Self(offset));
157 }
158
159 if matches!(s.to_lowercase().as_str(), "0" | "utc") {
160 let offset = UtcOffset::UTC;
161 return Ok(Self(offset));
162 }
163
164 if let Ok(offset) = UtcOffset::parse(s, OFFSET_FMT) {
166 return Ok(Self(offset));
167 }
168
169 bail!(r#""{s}" is not a valid timezone spec"#)
175 }
176}
177
178#[derive(Clone, Debug, Deserialize, Copy, Serialize)]
179pub enum Style {
180 #[serde(rename = "auto")]
181 Auto,
182
183 #[serde(rename = "full")]
184 Full,
185
186 #[serde(rename = "compact")]
187 Compact,
188}
189
190#[derive(Clone, Debug, Deserialize, Copy, Serialize)]
191pub enum WordJumpMode {
192 #[serde(rename = "emacs")]
193 Emacs,
194
195 #[serde(rename = "subl")]
196 Subl,
197}
198
199#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)]
200pub enum KeymapMode {
201 #[serde(rename = "emacs")]
202 Emacs,
203
204 #[serde(rename = "vim-normal")]
205 VimNormal,
206
207 #[serde(rename = "vim-insert")]
208 VimInsert,
209
210 #[serde(rename = "auto")]
211 Auto,
212}
213
214impl KeymapMode {
215 pub fn as_str(&self) -> &'static str {
216 match self {
217 KeymapMode::Emacs => "EMACS",
218 KeymapMode::VimNormal => "VIMNORMAL",
219 KeymapMode::VimInsert => "VIMINSERT",
220 KeymapMode::Auto => "AUTO",
221 }
222 }
223}
224
225#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)]
231pub enum CursorStyle {
232 #[serde(rename = "default")]
233 DefaultUserShape,
234
235 #[serde(rename = "blink-block")]
236 BlinkingBlock,
237
238 #[serde(rename = "steady-block")]
239 SteadyBlock,
240
241 #[serde(rename = "blink-underline")]
242 BlinkingUnderScore,
243
244 #[serde(rename = "steady-underline")]
245 SteadyUnderScore,
246
247 #[serde(rename = "blink-bar")]
248 BlinkingBar,
249
250 #[serde(rename = "steady-bar")]
251 SteadyBar,
252}
253
254impl CursorStyle {
255 pub fn as_str(&self) -> &'static str {
256 match self {
257 CursorStyle::DefaultUserShape => "DEFAULT",
258 CursorStyle::BlinkingBlock => "BLINKBLOCK",
259 CursorStyle::SteadyBlock => "STEADYBLOCK",
260 CursorStyle::BlinkingUnderScore => "BLINKUNDERLINE",
261 CursorStyle::SteadyUnderScore => "STEADYUNDERLINE",
262 CursorStyle::BlinkingBar => "BLINKBAR",
263 CursorStyle::SteadyBar => "STEADYBAR",
264 }
265 }
266}
267
268#[derive(Clone, Debug, Deserialize, Serialize)]
269pub struct Stats {
270 #[serde(default = "Stats::common_prefix_default")]
271 pub common_prefix: Vec<String>, #[serde(default = "Stats::common_subcommands_default")]
273 pub common_subcommands: Vec<String>, #[serde(default = "Stats::ignored_commands_default")]
275 pub ignored_commands: Vec<String>, }
277
278impl Stats {
279 fn common_prefix_default() -> Vec<String> {
280 vec!["sudo", "doas"].into_iter().map(String::from).collect()
281 }
282
283 fn common_subcommands_default() -> Vec<String> {
284 vec![
285 "apt",
286 "cargo",
287 "composer",
288 "dnf",
289 "docker",
290 "git",
291 "go",
292 "ip",
293 "kubectl",
294 "nix",
295 "nmcli",
296 "npm",
297 "pecl",
298 "pnpm",
299 "podman",
300 "port",
301 "systemctl",
302 "tmux",
303 "yarn",
304 ]
305 .into_iter()
306 .map(String::from)
307 .collect()
308 }
309
310 fn ignored_commands_default() -> Vec<String> {
311 vec![]
312 }
313}
314
315impl Default for Stats {
316 fn default() -> Self {
317 Self {
318 common_prefix: Self::common_prefix_default(),
319 common_subcommands: Self::common_subcommands_default(),
320 ignored_commands: Self::ignored_commands_default(),
321 }
322 }
323}
324
325#[derive(Clone, Debug, Deserialize, Default, Serialize)]
326pub struct Sync {
327 pub records: bool,
328}
329
330#[derive(Clone, Debug, Deserialize, Default, Serialize)]
331pub struct Keys {
332 pub scroll_exits: bool,
333 pub prefix: String,
334}
335
336#[derive(Clone, Debug, Deserialize, Serialize)]
337pub struct Preview {
338 pub strategy: PreviewStrategy,
339}
340
341#[derive(Clone, Debug, Deserialize, Serialize)]
342pub struct Theme {
343 pub name: String,
345
346 pub debug: Option<bool>,
348
349 pub max_depth: Option<u8>,
351}
352
353#[derive(Clone, Debug, Deserialize, Serialize)]
354pub struct Daemon {
355 #[serde(alias = "enable")]
358 pub enabled: bool,
359
360 pub sync_frequency: u64,
362
363 pub socket_path: String,
365
366 pub systemd_socket: bool,
368
369 pub tcp_port: u64,
371}
372
373#[derive(Clone, Debug, Deserialize, Serialize)]
374pub struct Search {
375 pub filters: Vec<FilterMode>,
377}
378
379impl Default for Preview {
380 fn default() -> Self {
381 Self {
382 strategy: PreviewStrategy::Auto,
383 }
384 }
385}
386
387impl Default for Theme {
388 fn default() -> Self {
389 Self {
390 name: "".to_string(),
391 debug: None::<bool>,
392 max_depth: Some(10),
393 }
394 }
395}
396
397impl Default for Daemon {
398 fn default() -> Self {
399 Self {
400 enabled: false,
401 sync_frequency: 300,
402 socket_path: "".to_string(),
403 systemd_socket: false,
404 tcp_port: 8889,
405 }
406 }
407}
408
409impl Default for Search {
410 fn default() -> Self {
411 Self {
412 filters: vec![
413 FilterMode::Global,
414 FilterMode::Host,
415 FilterMode::Session,
416 FilterMode::Workspace,
417 FilterMode::Directory,
418 ],
419 }
420 }
421}
422
423#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum, Serialize)]
425pub enum PreviewStrategy {
426 #[serde(rename = "auto")]
428 Auto,
429
430 #[serde(rename = "static")]
432 Static,
433
434 #[serde(rename = "fixed")]
436 Fixed,
437}
438
439#[derive(Clone, Debug, Deserialize, Serialize)]
440pub struct Settings {
441 pub dialect: Dialect,
442 pub timezone: Timezone,
443 pub style: Style,
444 pub auto_sync: bool,
445 pub update_check: bool,
446 pub sync_address: String,
447 pub sync_frequency: String,
448 pub db_path: String,
449 pub record_store_path: String,
450 pub key_path: String,
451 pub session_path: String,
452 pub search_mode: SearchMode,
453 pub filter_mode: Option<FilterMode>,
454 pub filter_mode_shell_up_key_binding: Option<FilterMode>,
455 pub search_mode_shell_up_key_binding: Option<SearchMode>,
456 pub shell_up_key_binding: bool,
457 pub inline_height: u16,
458 pub invert: bool,
459 pub show_preview: bool,
460 pub max_preview_height: u16,
461 pub show_help: bool,
462 pub show_tabs: bool,
463 pub auto_hide_height: u16,
464 pub exit_mode: ExitMode,
465 pub keymap_mode: KeymapMode,
466 pub keymap_mode_shell: KeymapMode,
467 pub keymap_cursor: HashMap<String, CursorStyle>,
468 pub word_jump_mode: WordJumpMode,
469 pub word_chars: String,
470 pub scroll_context_lines: usize,
471 pub history_format: String,
472 pub prefers_reduced_motion: bool,
473 pub store_failed: bool,
474
475 #[serde(with = "serde_regex", default = "RegexSet::empty", skip_serializing)]
476 pub history_filter: RegexSet,
477
478 #[serde(with = "serde_regex", default = "RegexSet::empty", skip_serializing)]
479 pub cwd_filter: RegexSet,
480
481 pub secrets_filter: bool,
482 pub workspaces: bool,
483 pub ctrl_n_shortcuts: bool,
484
485 pub network_connect_timeout: u64,
486 pub network_timeout: u64,
487 pub local_timeout: f64,
488 pub enter_accept: bool,
489 pub smart_sort: bool,
490
491 #[serde(default)]
492 pub stats: Stats,
493
494 #[serde(default)]
495 pub sync: Sync,
496
497 #[serde(default)]
498 pub keys: Keys,
499
500 #[serde(default)]
501 pub preview: Preview,
502
503 #[serde(default)]
504 pub dotfiles: dotfiles::Settings,
505
506 #[serde(default)]
507 pub daemon: Daemon,
508
509 #[serde(default)]
510 pub search: Search,
511
512 #[serde(default)]
513 pub theme: Theme,
514}
515
516impl Settings {
517 pub fn utc() -> Self {
518 Self::builder()
519 .expect("Could not build default")
520 .set_override("timezone", "0")
521 .expect("failed to override timezone with UTC")
522 .build()
523 .expect("Could not build config")
524 .try_deserialize()
525 .expect("Could not deserialize config")
526 }
527
528 fn save_to_data_dir(filename: &str, value: &str) -> Result<()> {
529 let data_dir = atuin_common::utils::data_dir();
530 let data_dir = data_dir.as_path();
531
532 let path = data_dir.join(filename);
533
534 fs_err::write(path, value)?;
535
536 Ok(())
537 }
538
539 fn read_from_data_dir(filename: &str) -> Option<String> {
540 let data_dir = atuin_common::utils::data_dir();
541 let data_dir = data_dir.as_path();
542
543 let path = data_dir.join(filename);
544
545 if !path.exists() {
546 return None;
547 }
548
549 let value = fs_err::read_to_string(path);
550
551 value.ok()
552 }
553
554 fn save_current_time(filename: &str) -> Result<()> {
555 Settings::save_to_data_dir(
556 filename,
557 OffsetDateTime::now_utc().format(&Rfc3339)?.as_str(),
558 )?;
559
560 Ok(())
561 }
562
563 fn load_time_from_file(filename: &str) -> Result<OffsetDateTime> {
564 let value = Settings::read_from_data_dir(filename);
565
566 match value {
567 Some(v) => Ok(OffsetDateTime::parse(v.as_str(), &Rfc3339)?),
568 None => Ok(OffsetDateTime::UNIX_EPOCH),
569 }
570 }
571
572 pub fn save_sync_time() -> Result<()> {
573 Settings::save_current_time(LAST_SYNC_FILENAME)
574 }
575
576 pub fn save_version_check_time() -> Result<()> {
577 Settings::save_current_time(LAST_VERSION_CHECK_FILENAME)
578 }
579
580 pub fn last_sync() -> Result<OffsetDateTime> {
581 Settings::load_time_from_file(LAST_SYNC_FILENAME)
582 }
583
584 pub fn last_version_check() -> Result<OffsetDateTime> {
585 Settings::load_time_from_file(LAST_VERSION_CHECK_FILENAME)
586 }
587
588 pub fn host_id() -> Option<HostId> {
589 let id = Settings::read_from_data_dir(HOST_ID_FILENAME);
590
591 if let Some(id) = id {
592 let parsed =
593 Uuid::from_str(id.as_str()).expect("failed to parse host ID from local directory");
594 return Some(HostId(parsed));
595 }
596
597 let uuid = atuin_common::utils::uuid_v7();
598
599 Settings::save_to_data_dir(HOST_ID_FILENAME, uuid.as_simple().to_string().as_ref())
600 .expect("Could not write host ID to data dir");
601
602 Some(HostId(uuid))
603 }
604
605 pub fn should_sync(&self) -> Result<bool> {
606 if !self.auto_sync || !PathBuf::from(self.session_path.as_str()).exists() {
607 return Ok(false);
608 }
609
610 if self.sync_frequency == "0" {
611 return Ok(true);
612 }
613
614 match parse_duration(self.sync_frequency.as_str()) {
615 Ok(d) => {
616 let d = time::Duration::try_from(d).unwrap();
617 Ok(OffsetDateTime::now_utc() - Settings::last_sync()? >= d)
618 }
619 Err(e) => Err(eyre!("failed to check sync: {}", e)),
620 }
621 }
622
623 pub fn logged_in(&self) -> bool {
624 let session_path = self.session_path.as_str();
625
626 PathBuf::from(session_path).exists()
627 }
628
629 pub fn session_token(&self) -> Result<String> {
630 if !self.logged_in() {
631 return Err(eyre!("Tried to load session; not logged in"));
632 }
633
634 let session_path = self.session_path.as_str();
635 Ok(fs_err::read_to_string(session_path)?)
636 }
637
638 #[cfg(feature = "check-update")]
639 fn needs_update_check(&self) -> Result<bool> {
640 let last_check = Settings::last_version_check()?;
641 let diff = OffsetDateTime::now_utc() - last_check;
642
643 Ok(diff.whole_hours() >= 1)
645 }
646
647 #[cfg(feature = "check-update")]
648 async fn latest_version(&self) -> Result<Version> {
649 let current =
652 Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or(Version::new(100000, 0, 0));
653
654 if !self.needs_update_check()? {
655 let version = tokio::task::spawn_blocking(|| {
658 Settings::read_from_data_dir(LATEST_VERSION_FILENAME)
659 })
660 .await
661 .expect("file task panicked");
662
663 let version = match version {
664 Some(v) => Version::parse(&v).unwrap_or(current),
665 None => current,
666 };
667
668 return Ok(version);
669 }
670
671 #[cfg(feature = "sync")]
672 let latest = crate::api_client::latest_version().await.unwrap_or(current);
673
674 #[cfg(not(feature = "sync"))]
675 let latest = current;
676
677 let latest_encoded = latest.to_string();
678 tokio::task::spawn_blocking(move || {
679 Settings::save_version_check_time()?;
680 Settings::save_to_data_dir(LATEST_VERSION_FILENAME, &latest_encoded)?;
681 Ok::<(), eyre::Report>(())
682 })
683 .await
684 .expect("file task panicked")?;
685
686 Ok(latest)
687 }
688
689 #[cfg(feature = "check-update")]
691 pub async fn needs_update(&self) -> Option<Version> {
692 if !self.update_check {
693 return None;
694 }
695
696 let current =
697 Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or(Version::new(100000, 0, 0));
698
699 let latest = self.latest_version().await;
700
701 if latest.is_err() {
702 return None;
703 }
704
705 let latest = latest.unwrap();
706
707 if latest > current {
708 return Some(latest);
709 }
710
711 None
712 }
713
714 pub fn default_filter_mode(&self) -> FilterMode {
715 self.filter_mode
716 .filter(|x| self.search.filters.contains(x))
717 .or(self.search.filters.first().copied())
718 .unwrap_or(FilterMode::Global)
719 }
720
721 #[cfg(not(feature = "check-update"))]
722 pub async fn needs_update(&self) -> Option<Version> {
723 None
724 }
725
726 pub fn builder() -> Result<ConfigBuilder<DefaultState>> {
727 let data_dir = atuin_common::utils::data_dir();
728 let db_path = data_dir.join("history.db");
729 let record_store_path = data_dir.join("records.db");
730 let socket_path = atuin_common::utils::runtime_dir().join("atuin.sock");
731
732 let key_path = data_dir.join("key");
733 let session_path = data_dir.join("session");
734
735 Ok(Config::builder()
736 .set_default("history_format", "{time}\t{command}\t{duration}")?
737 .set_default("db_path", db_path.to_str())?
738 .set_default("record_store_path", record_store_path.to_str())?
739 .set_default("key_path", key_path.to_str())?
740 .set_default("session_path", session_path.to_str())?
741 .set_default("dialect", "us")?
742 .set_default("timezone", "local")?
743 .set_default("auto_sync", true)?
744 .set_default("update_check", cfg!(feature = "check-update"))?
745 .set_default("sync_address", "https://api.atuin.sh")?
746 .set_default("sync_frequency", "10m")?
747 .set_default("search_mode", "fuzzy")?
748 .set_default("filter_mode", None::<String>)?
749 .set_default("style", "compact")?
750 .set_default("inline_height", 40)?
751 .set_default("show_preview", true)?
752 .set_default("preview.strategy", "auto")?
753 .set_default("max_preview_height", 4)?
754 .set_default("show_help", true)?
755 .set_default("show_tabs", true)?
756 .set_default("auto_hide_height", 8)?
757 .set_default("invert", false)?
758 .set_default("exit_mode", "return-original")?
759 .set_default("word_jump_mode", "emacs")?
760 .set_default(
761 "word_chars",
762 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
763 )?
764 .set_default("scroll_context_lines", 1)?
765 .set_default("shell_up_key_binding", false)?
766 .set_default("workspaces", false)?
767 .set_default("ctrl_n_shortcuts", false)?
768 .set_default("secrets_filter", true)?
769 .set_default("network_connect_timeout", 5)?
770 .set_default("network_timeout", 30)?
771 .set_default("local_timeout", 2.0)?
772 .set_default("enter_accept", false)?
778 .set_default("sync.records", true)?
779 .set_default("keys.scroll_exits", true)?
780 .set_default("keys.prefix", "a")?
781 .set_default("keymap_mode", "emacs")?
782 .set_default("keymap_mode_shell", "auto")?
783 .set_default("keymap_cursor", HashMap::<String, String>::new())?
784 .set_default("smart_sort", false)?
785 .set_default("store_failed", true)?
786 .set_default("daemon.sync_frequency", 300)?
787 .set_default("daemon.enabled", false)?
788 .set_default("daemon.socket_path", socket_path.to_str())?
789 .set_default("daemon.systemd_socket", false)?
790 .set_default("daemon.tcp_port", 8889)?
791 .set_default(
792 "search.filters",
793 vec!["global", "host", "session", "workspace", "directory"],
794 )?
795 .set_default("theme.name", "default")?
796 .set_default("theme.debug", None::<bool>)?
797 .set_default(
798 "prefers_reduced_motion",
799 std::env::var("NO_MOTION")
800 .ok()
801 .map(|_| config::Value::new(None, config::ValueKind::Boolean(true)))
802 .unwrap_or_else(|| config::Value::new(None, config::ValueKind::Boolean(false))),
803 )?
804 .add_source(
805 Environment::with_prefix("atuin")
806 .prefix_separator("_")
807 .separator("__"),
808 ))
809 }
810
811 pub fn new() -> Result<Self> {
812 let config_dir = atuin_common::utils::config_dir();
813 let data_dir = atuin_common::utils::data_dir();
814
815 create_dir_all(&config_dir)
816 .wrap_err_with(|| format!("could not create dir {config_dir:?}"))?;
817
818 create_dir_all(&data_dir).wrap_err_with(|| format!("could not create dir {data_dir:?}"))?;
819
820 let mut config_file = if let Ok(p) = std::env::var("ATUIN_CONFIG_DIR") {
821 PathBuf::from(p)
822 } else {
823 let mut config_file = PathBuf::new();
824 config_file.push(config_dir);
825 config_file
826 };
827
828 config_file.push("config.toml");
829
830 let mut config_builder = Self::builder()?;
831
832 config_builder = if config_file.exists() {
833 config_builder.add_source(ConfigFile::new(
834 config_file.to_str().unwrap(),
835 FileFormat::Toml,
836 ))
837 } else {
838 let mut file = File::create(config_file).wrap_err("could not create config file")?;
839 file.write_all(EXAMPLE_CONFIG.as_bytes())
840 .wrap_err("could not write default config file")?;
841
842 config_builder
843 };
844
845 let config = config_builder.build()?;
846 let mut settings: Settings = config
847 .try_deserialize()
848 .map_err(|e| eyre!("failed to deserialize: {}", e))?;
849
850 let db_path = settings.db_path;
852 let db_path = shellexpand::full(&db_path)?;
853 settings.db_path = db_path.to_string();
854
855 let key_path = settings.key_path;
856 let key_path = shellexpand::full(&key_path)?;
857 settings.key_path = key_path.to_string();
858
859 let session_path = settings.session_path;
860 let session_path = shellexpand::full(&session_path)?;
861 settings.session_path = session_path.to_string();
862
863 Ok(settings)
864 }
865
866 pub fn example_config() -> &'static str {
867 EXAMPLE_CONFIG
868 }
869}
870
871impl Default for Settings {
872 fn default() -> Self {
873 Self::builder()
876 .expect("Could not build default")
877 .build()
878 .expect("Could not build config")
879 .try_deserialize()
880 .expect("Could not deserialize config")
881 }
882}
883
884#[cfg(test)]
885pub(crate) fn test_local_timeout() -> f64 {
886 std::env::var("ATUIN_TEST_LOCAL_TIMEOUT")
887 .ok()
888 .and_then(|x| x.parse().ok())
889 .unwrap_or(2.0)
892}
893
894#[cfg(test)]
895mod tests {
896 use std::str::FromStr;
897
898 use eyre::Result;
899
900 use super::Timezone;
901
902 #[test]
903 fn can_parse_offset_timezone_spec() -> Result<()> {
904 assert_eq!(Timezone::from_str("+02")?.0.as_hms(), (2, 0, 0));
905 assert_eq!(Timezone::from_str("-04")?.0.as_hms(), (-4, 0, 0));
906 assert_eq!(Timezone::from_str("+05:30")?.0.as_hms(), (5, 30, 0));
907 assert_eq!(Timezone::from_str("-09:30")?.0.as_hms(), (-9, -30, 0));
908
909 assert_eq!(Timezone::from_str("+2")?.0.as_hms(), (2, 0, 0));
911 assert_eq!(Timezone::from_str("-4")?.0.as_hms(), (-4, 0, 0));
912 assert_eq!(Timezone::from_str("+5:30")?.0.as_hms(), (5, 30, 0));
913 assert_eq!(Timezone::from_str("-9:30")?.0.as_hms(), (-9, -30, 0));
914
915 assert_eq!(Timezone::from_str("+09:30:00")?.0.as_hms(), (9, 30, 0));
917 assert_eq!(Timezone::from_str("-09:30:00")?.0.as_hms(), (-9, -30, 0));
918
919 assert_eq!(Timezone::from_str("+0:5")?.0.as_hms(), (0, 5, 0));
921 assert_eq!(Timezone::from_str("-0:5")?.0.as_hms(), (0, -5, 0));
922 assert_eq!(Timezone::from_str("+01:23:45")?.0.as_hms(), (1, 23, 45));
923 assert_eq!(Timezone::from_str("-01:23:45")?.0.as_hms(), (-1, -23, -45));
924
925 assert!(Timezone::from_str("5").is_err());
927 assert!(Timezone::from_str("10:30").is_err());
928
929 Ok(())
930 }
931}