mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-22 00:03:49 +01:00
settings refactor
This commit is contained in:
parent
dd587201ca
commit
32f0920463
@ -1,32 +1,41 @@
|
|||||||
|
mod behaviour;
|
||||||
|
pub mod display;
|
||||||
|
mod input;
|
||||||
|
mod stats;
|
||||||
|
mod sync;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
use ::time as time_lib;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
convert::TryFrom,
|
|
||||||
fmt,
|
|
||||||
io::prelude::*,
|
io::prelude::*,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use atuin_common::record::HostId;
|
use atuin_common::record::HostId;
|
||||||
use clap::ValueEnum;
|
|
||||||
use config::{
|
use config::{
|
||||||
builder::DefaultState, Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat,
|
builder::DefaultState, Config, ConfigBuilder, Environment, File as ConfigFile, FileFormat,
|
||||||
};
|
};
|
||||||
use eyre::{bail, eyre, Context, Error, Result};
|
use eyre::{eyre, Context, Result};
|
||||||
use fs_err::{create_dir_all, File};
|
use fs_err::{create_dir_all, File};
|
||||||
use parse_duration::parse;
|
use parse_duration::parse;
|
||||||
use ratatui::style::{Color, Stylize};
|
|
||||||
use regex::RegexSet;
|
use regex::RegexSet;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::Deserialize;
|
||||||
use serde_with::DeserializeFromStr;
|
use time_lib::{format_description::well_known::Rfc3339, Duration, OffsetDateTime};
|
||||||
use time::{
|
|
||||||
format_description::{well_known::Rfc3339, FormatItem},
|
|
||||||
macros::format_description,
|
|
||||||
OffsetDateTime, UtcOffset,
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
behaviour::{ExitMode, FilterMode, SearchMode},
|
||||||
|
display::{Display, Styles},
|
||||||
|
input::{CursorStyle, KeymapMode, Keys, WordJumpMode},
|
||||||
|
stats::{Dialect, Stats},
|
||||||
|
sync::Sync,
|
||||||
|
time::Timezone,
|
||||||
|
};
|
||||||
|
|
||||||
pub const HISTORY_PAGE_SIZE: i64 = 100;
|
pub const HISTORY_PAGE_SIZE: i64 = 100;
|
||||||
pub const LAST_SYNC_FILENAME: &str = "last_sync_time";
|
pub const LAST_SYNC_FILENAME: &str = "last_sync_time";
|
||||||
pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time";
|
pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time";
|
||||||
@ -34,464 +43,68 @@ pub const LATEST_VERSION_FILENAME: &str = "latest_version";
|
|||||||
pub const HOST_ID_FILENAME: &str = "host_id";
|
pub const HOST_ID_FILENAME: &str = "host_id";
|
||||||
static EXAMPLE_CONFIG: &str = include_str!("../config.toml");
|
static EXAMPLE_CONFIG: &str = include_str!("../config.toml");
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy, ValueEnum, PartialEq)]
|
|
||||||
pub enum SearchMode {
|
|
||||||
#[serde(rename = "prefix")]
|
|
||||||
Prefix,
|
|
||||||
|
|
||||||
#[serde(rename = "fulltext")]
|
|
||||||
#[clap(aliases = &["fulltext"])]
|
|
||||||
FullText,
|
|
||||||
|
|
||||||
#[serde(rename = "fuzzy")]
|
|
||||||
Fuzzy,
|
|
||||||
|
|
||||||
#[serde(rename = "skim")]
|
|
||||||
Skim,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchMode {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
SearchMode::Prefix => "PREFIX",
|
|
||||||
SearchMode::FullText => "FULLTXT",
|
|
||||||
SearchMode::Fuzzy => "FUZZY",
|
|
||||||
SearchMode::Skim => "SKIM",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn next(&self, settings: &Settings) -> Self {
|
|
||||||
match self {
|
|
||||||
SearchMode::Prefix => SearchMode::FullText,
|
|
||||||
// if the user is using skim, we go to skim
|
|
||||||
SearchMode::FullText if settings.search_mode == SearchMode::Skim => SearchMode::Skim,
|
|
||||||
// otherwise fuzzy.
|
|
||||||
SearchMode::FullText => SearchMode::Fuzzy,
|
|
||||||
SearchMode::Fuzzy | SearchMode::Skim => SearchMode::Prefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]
|
|
||||||
pub enum FilterMode {
|
|
||||||
#[serde(rename = "global")]
|
|
||||||
Global = 0,
|
|
||||||
|
|
||||||
#[serde(rename = "host")]
|
|
||||||
Host = 1,
|
|
||||||
|
|
||||||
#[serde(rename = "session")]
|
|
||||||
Session = 2,
|
|
||||||
|
|
||||||
#[serde(rename = "directory")]
|
|
||||||
Directory = 3,
|
|
||||||
|
|
||||||
#[serde(rename = "workspace")]
|
|
||||||
Workspace = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FilterMode {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
FilterMode::Global => "GLOBAL",
|
|
||||||
FilterMode::Host => "HOST",
|
|
||||||
FilterMode::Session => "SESSION",
|
|
||||||
FilterMode::Directory => "DIRECTORY",
|
|
||||||
FilterMode::Workspace => "WORKSPACE",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy)]
|
|
||||||
pub enum ExitMode {
|
|
||||||
#[serde(rename = "return-original")]
|
|
||||||
ReturnOriginal,
|
|
||||||
|
|
||||||
#[serde(rename = "return-query")]
|
|
||||||
ReturnQuery,
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Can use upstream Dialect enum if https://github.com/stevedonovan/chrono-english/pull/16 is merged
|
|
||||||
// FIXME: Above PR was merged, but dependency was changed to interim (fork of chrono-english) in the ... interim
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy)]
|
|
||||||
pub enum Dialect {
|
|
||||||
#[serde(rename = "us")]
|
|
||||||
Us,
|
|
||||||
|
|
||||||
#[serde(rename = "uk")]
|
|
||||||
Uk,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Dialect> for interim::Dialect {
|
|
||||||
fn from(d: Dialect) -> interim::Dialect {
|
|
||||||
match d {
|
|
||||||
Dialect::Uk => interim::Dialect::Uk,
|
|
||||||
Dialect::Us => interim::Dialect::Us,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type wrapper around `time::UtcOffset` to support a wider variety of timezone formats.
|
|
||||||
///
|
|
||||||
/// Note that the parsing of this struct needs to be done before starting any
|
|
||||||
/// multithreaded runtime, otherwise it will fail on most Unix systems.
|
|
||||||
///
|
|
||||||
/// See: https://github.com/atuinsh/atuin/pull/1517#discussion_r1447516426
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, DeserializeFromStr)]
|
|
||||||
pub struct Timezone(pub UtcOffset);
|
|
||||||
impl fmt::Display for Timezone {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// format: <+|-><hour>[:<minute>[:<second>]]
|
|
||||||
static OFFSET_FMT: &[FormatItem<'_>] =
|
|
||||||
format_description!("[offset_hour sign:mandatory padding:none][optional [:[offset_minute padding:none][optional [:[offset_second padding:none]]]]]");
|
|
||||||
impl FromStr for Timezone {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
// local timezone
|
|
||||||
if matches!(s.to_lowercase().as_str(), "l" | "local") {
|
|
||||||
let offset = UtcOffset::current_local_offset()?;
|
|
||||||
return Ok(Self(offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(s.to_lowercase().as_str(), "0" | "utc") {
|
|
||||||
let offset = UtcOffset::UTC;
|
|
||||||
return Ok(Self(offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// offset from UTC
|
|
||||||
if let Ok(offset) = UtcOffset::parse(s, OFFSET_FMT) {
|
|
||||||
return Ok(Self(offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
// IDEA: Currently named timezones are not supported, because the well-known crate
|
|
||||||
// for this is `chrono_tz`, which is not really interoperable with the datetime crate
|
|
||||||
// that we currently use - `time`. If ever we migrate to using `chrono`, this would
|
|
||||||
// be a good feature to add.
|
|
||||||
|
|
||||||
bail!(r#""{s}" is not a valid timezone spec"#)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy)]
|
|
||||||
pub enum Style {
|
|
||||||
#[serde(rename = "auto")]
|
|
||||||
Auto,
|
|
||||||
|
|
||||||
#[serde(rename = "full")]
|
|
||||||
Full,
|
|
||||||
|
|
||||||
#[serde(rename = "compact")]
|
|
||||||
Compact,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy)]
|
|
||||||
pub enum WordJumpMode {
|
|
||||||
#[serde(rename = "emacs")]
|
|
||||||
Emacs,
|
|
||||||
|
|
||||||
#[serde(rename = "subl")]
|
|
||||||
Subl,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]
|
|
||||||
pub enum KeymapMode {
|
|
||||||
#[serde(rename = "emacs")]
|
|
||||||
Emacs,
|
|
||||||
|
|
||||||
#[serde(rename = "vim-normal")]
|
|
||||||
VimNormal,
|
|
||||||
|
|
||||||
#[serde(rename = "vim-insert")]
|
|
||||||
VimInsert,
|
|
||||||
|
|
||||||
#[serde(rename = "auto")]
|
|
||||||
Auto,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl KeymapMode {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
KeymapMode::Emacs => "EMACS",
|
|
||||||
KeymapMode::VimNormal => "VIMNORMAL",
|
|
||||||
KeymapMode::VimInsert => "VIMINSERT",
|
|
||||||
KeymapMode::Auto => "AUTO",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to translate the config to crossterm::cursor::SetCursorStyle, but
|
|
||||||
// the original type does not implement trait serde::Deserialize unfortunately.
|
|
||||||
// It seems impossible to implement Deserialize for external types when it is
|
|
||||||
// used in HashMap (https://stackoverflow.com/questions/67142663). We instead
|
|
||||||
// define an adapter type.
|
|
||||||
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]
|
|
||||||
pub enum CursorStyle {
|
|
||||||
#[serde(rename = "default")]
|
|
||||||
DefaultUserShape,
|
|
||||||
|
|
||||||
#[serde(rename = "blink-block")]
|
|
||||||
BlinkingBlock,
|
|
||||||
|
|
||||||
#[serde(rename = "steady-block")]
|
|
||||||
SteadyBlock,
|
|
||||||
|
|
||||||
#[serde(rename = "blink-underline")]
|
|
||||||
BlinkingUnderScore,
|
|
||||||
|
|
||||||
#[serde(rename = "steady-underline")]
|
|
||||||
SteadyUnderScore,
|
|
||||||
|
|
||||||
#[serde(rename = "blink-bar")]
|
|
||||||
BlinkingBar,
|
|
||||||
|
|
||||||
#[serde(rename = "steady-bar")]
|
|
||||||
SteadyBar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CursorStyle {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
CursorStyle::DefaultUserShape => "DEFAULT",
|
|
||||||
CursorStyle::BlinkingBlock => "BLINKBLOCK",
|
|
||||||
CursorStyle::SteadyBlock => "STEADYBLOCK",
|
|
||||||
CursorStyle::BlinkingUnderScore => "BLINKUNDERLINE",
|
|
||||||
CursorStyle::SteadyUnderScore => "STEADYUNDERLINE",
|
|
||||||
CursorStyle::BlinkingBar => "BLINKBAR",
|
|
||||||
CursorStyle::SteadyBar => "STEADYBAR",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct Stats {
|
|
||||||
#[serde(default = "Stats::common_prefix_default")]
|
|
||||||
pub common_prefix: Vec<String>, // sudo, etc. commands we want to strip off
|
|
||||||
#[serde(default = "Stats::common_subcommands_default")]
|
|
||||||
pub common_subcommands: Vec<String>, // kubectl, commands we should consider subcommands for
|
|
||||||
#[serde(default = "Stats::ignored_commands_default")]
|
|
||||||
pub ignored_commands: Vec<String>, // cd, ls, etc. commands we want to completely hide from stats
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Stats {
|
|
||||||
fn common_prefix_default() -> Vec<String> {
|
|
||||||
vec!["sudo", "doas"].into_iter().map(String::from).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn common_subcommands_default() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
"apt",
|
|
||||||
"cargo",
|
|
||||||
"composer",
|
|
||||||
"dnf",
|
|
||||||
"docker",
|
|
||||||
"git",
|
|
||||||
"go",
|
|
||||||
"ip",
|
|
||||||
"kubectl",
|
|
||||||
"nix",
|
|
||||||
"nmcli",
|
|
||||||
"npm",
|
|
||||||
"pecl",
|
|
||||||
"pnpm",
|
|
||||||
"podman",
|
|
||||||
"port",
|
|
||||||
"systemctl",
|
|
||||||
"tmux",
|
|
||||||
"yarn",
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(String::from)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ignored_commands_default() -> Vec<String> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Stats {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
common_prefix: Self::common_prefix_default(),
|
|
||||||
common_subcommands: Self::common_subcommands_default(),
|
|
||||||
ignored_commands: Self::ignored_commands_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
|
||||||
pub struct Styles {
|
|
||||||
#[serde(default, deserialize_with = "Variants::deserialize_style")]
|
|
||||||
pub command: Option<ratatui::style::Style>,
|
|
||||||
#[serde(default, deserialize_with = "Variants::deserialize_style")]
|
|
||||||
pub command_selected: Option<ratatui::style::Style>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum Variants {
|
|
||||||
Color(Color),
|
|
||||||
Components(Components),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Variants {
|
|
||||||
fn deserialize_style<'de, D>(deserializer: D) -> Result<Option<ratatui::style::Style>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let variants: Option<Variants> = Deserialize::deserialize(deserializer)?;
|
|
||||||
let style: Option<ratatui::style::Style> = variants.map(|variants| variants.into());
|
|
||||||
|
|
||||||
Ok(style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Variants> for ratatui::style::Style {
|
|
||||||
fn from(value: Variants) -> ratatui::style::Style {
|
|
||||||
match value {
|
|
||||||
Variants::Components(complex_style) => complex_style.into(),
|
|
||||||
Variants::Color(color) => color.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
|
||||||
pub struct Components {
|
|
||||||
// Colors
|
|
||||||
#[serde(default)]
|
|
||||||
pub foreground: Option<Color>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub background: Option<Color>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub underline: Option<Color>,
|
|
||||||
|
|
||||||
// Modifiers
|
|
||||||
#[serde(default)]
|
|
||||||
pub bold: Option<bool>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub crossed_out: Option<bool>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub italic: Option<bool>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub underlined: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Components> for ratatui::style::Style {
|
|
||||||
fn from(value: Components) -> ratatui::style::Style {
|
|
||||||
let mut style = ratatui::style::Style::default();
|
|
||||||
|
|
||||||
if let Some(color) = value.foreground {
|
|
||||||
style = style.fg(color);
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(color) = value.background {
|
|
||||||
style = style.bg(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(color) = value.underline {
|
|
||||||
style = style.underline_color(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
style = match value.bold {
|
|
||||||
Some(true) => style.bold(),
|
|
||||||
Some(_) => style.not_bold(),
|
|
||||||
_ => style,
|
|
||||||
};
|
|
||||||
|
|
||||||
style = match value.crossed_out {
|
|
||||||
Some(true) => style.crossed_out(),
|
|
||||||
Some(_) => style.not_crossed_out(),
|
|
||||||
_ => style,
|
|
||||||
};
|
|
||||||
|
|
||||||
style = match value.italic {
|
|
||||||
Some(true) => style.italic(),
|
|
||||||
Some(_) => style.not_italic(),
|
|
||||||
_ => style,
|
|
||||||
};
|
|
||||||
|
|
||||||
style = match value.underlined {
|
|
||||||
Some(true) => style.underlined(),
|
|
||||||
Some(_) => style.not_underlined(),
|
|
||||||
_ => style,
|
|
||||||
};
|
|
||||||
|
|
||||||
style
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
|
||||||
pub struct Sync {
|
|
||||||
pub records: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default)]
|
|
||||||
pub struct Keys {
|
|
||||||
pub scroll_exits: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub dialect: Dialect,
|
// Behaviour
|
||||||
pub timezone: Timezone,
|
pub exit_mode: ExitMode,
|
||||||
pub style: Style,
|
|
||||||
pub auto_sync: bool,
|
|
||||||
pub update_check: bool,
|
|
||||||
pub sync_address: String,
|
|
||||||
pub sync_frequency: String,
|
|
||||||
pub db_path: String,
|
|
||||||
pub record_store_path: String,
|
|
||||||
pub key_path: String,
|
|
||||||
pub session_path: String,
|
|
||||||
pub search_mode: SearchMode,
|
|
||||||
pub filter_mode: FilterMode,
|
pub filter_mode: FilterMode,
|
||||||
pub filter_mode_shell_up_key_binding: Option<FilterMode>,
|
pub filter_mode_shell_up_key_binding: Option<FilterMode>,
|
||||||
|
pub search_mode: SearchMode,
|
||||||
pub search_mode_shell_up_key_binding: Option<SearchMode>,
|
pub search_mode_shell_up_key_binding: Option<SearchMode>,
|
||||||
pub shell_up_key_binding: bool,
|
|
||||||
pub inline_height: u16,
|
|
||||||
pub invert: bool,
|
|
||||||
pub show_preview: bool,
|
|
||||||
pub max_preview_height: u16,
|
|
||||||
pub show_help: bool,
|
|
||||||
pub exit_mode: ExitMode,
|
|
||||||
pub keymap_mode: KeymapMode,
|
|
||||||
pub keymap_mode_shell: KeymapMode,
|
|
||||||
pub keymap_cursor: HashMap<String, CursorStyle>,
|
|
||||||
pub word_jump_mode: WordJumpMode,
|
|
||||||
pub word_chars: String,
|
|
||||||
pub scroll_context_lines: usize,
|
|
||||||
pub history_format: String,
|
|
||||||
pub prefers_reduced_motion: bool,
|
|
||||||
|
|
||||||
#[serde(with = "serde_regex", default = "RegexSet::empty")]
|
// Display
|
||||||
pub history_filter: RegexSet,
|
#[serde(default, flatten)]
|
||||||
|
pub display: display::Settings,
|
||||||
|
|
||||||
|
// Filters
|
||||||
#[serde(with = "serde_regex", default = "RegexSet::empty")]
|
#[serde(with = "serde_regex", default = "RegexSet::empty")]
|
||||||
pub cwd_filter: RegexSet,
|
pub cwd_filter: RegexSet,
|
||||||
|
#[serde(with = "serde_regex", default = "RegexSet::empty")]
|
||||||
|
pub history_filter: RegexSet,
|
||||||
pub secrets_filter: bool,
|
pub secrets_filter: bool,
|
||||||
pub workspaces: bool,
|
pub workspaces: bool,
|
||||||
pub ctrl_n_shortcuts: bool,
|
|
||||||
|
|
||||||
pub network_connect_timeout: u64,
|
// Input
|
||||||
pub network_timeout: u64,
|
|
||||||
pub local_timeout: f64,
|
|
||||||
pub enter_accept: bool,
|
pub enter_accept: bool,
|
||||||
|
pub keymap_cursor: HashMap<String, CursorStyle>,
|
||||||
|
pub keymap_mode: KeymapMode,
|
||||||
|
pub keymap_mode_shell: KeymapMode,
|
||||||
|
#[serde(default)]
|
||||||
|
pub keys: Keys,
|
||||||
|
pub shell_up_key_binding: bool,
|
||||||
|
pub word_jump_mode: WordJumpMode,
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
pub db_path: String,
|
||||||
|
pub key_path: String,
|
||||||
|
pub record_store_path: String,
|
||||||
|
pub session_path: String,
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
pub dialect: Dialect,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub stats: Stats,
|
pub stats: Stats,
|
||||||
|
|
||||||
#[serde(default)]
|
// Sync
|
||||||
pub styles: Styles,
|
pub auto_sync: bool,
|
||||||
|
pub sync_address: String,
|
||||||
|
pub sync_frequency: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sync: Sync,
|
pub sync: Sync,
|
||||||
|
|
||||||
#[serde(default)]
|
// Time
|
||||||
pub keys: Keys,
|
pub timezone: Timezone,
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
pub local_timeout: f64,
|
||||||
|
pub network_connect_timeout: u64,
|
||||||
|
pub network_timeout: u64,
|
||||||
|
|
||||||
|
pub update_check: bool,
|
||||||
|
pub word_chars: String,
|
||||||
|
pub scroll_context_lines: usize,
|
||||||
|
pub history_format: String,
|
||||||
|
pub ctrl_n_shortcuts: bool,
|
||||||
|
|
||||||
// This is automatically loaded when settings is created. Do not set in
|
// This is automatically loaded when settings is created. Do not set in
|
||||||
// config! Keep secrets and settings apart.
|
// config! Keep secrets and settings apart.
|
||||||
@ -595,7 +208,7 @@ impl Settings {
|
|||||||
|
|
||||||
match parse(self.sync_frequency.as_str()) {
|
match parse(self.sync_frequency.as_str()) {
|
||||||
Ok(d) => {
|
Ok(d) => {
|
||||||
let d = time::Duration::try_from(d).unwrap();
|
let d = Duration::try_from(d).unwrap();
|
||||||
Ok(OffsetDateTime::now_utc() - Settings::last_sync()? >= d)
|
Ok(OffsetDateTime::now_utc() - Settings::last_sync()? >= d)
|
||||||
}
|
}
|
||||||
Err(e) => Err(eyre!("failed to check sync: {}", e)),
|
Err(e) => Err(eyre!("failed to check sync: {}", e)),
|
||||||
@ -679,7 +292,10 @@ impl Settings {
|
|||||||
let key_path = data_dir.join("key");
|
let key_path = data_dir.join("key");
|
||||||
let session_path = data_dir.join("session");
|
let session_path = data_dir.join("session");
|
||||||
|
|
||||||
Ok(Config::builder()
|
let builder = Config::builder();
|
||||||
|
let builder = display::defaults(builder)?;
|
||||||
|
|
||||||
|
Ok(builder
|
||||||
.set_default("history_format", "{time}\t{command}\t{duration}")?
|
.set_default("history_format", "{time}\t{command}\t{duration}")?
|
||||||
.set_default("db_path", db_path.to_str())?
|
.set_default("db_path", db_path.to_str())?
|
||||||
.set_default("record_store_path", record_store_path.to_str())?
|
.set_default("record_store_path", record_store_path.to_str())?
|
||||||
@ -693,12 +309,6 @@ impl Settings {
|
|||||||
.set_default("sync_frequency", "10m")?
|
.set_default("sync_frequency", "10m")?
|
||||||
.set_default("search_mode", "fuzzy")?
|
.set_default("search_mode", "fuzzy")?
|
||||||
.set_default("filter_mode", "global")?
|
.set_default("filter_mode", "global")?
|
||||||
.set_default("style", "auto")?
|
|
||||||
.set_default("inline_height", 0)?
|
|
||||||
.set_default("show_preview", false)?
|
|
||||||
.set_default("max_preview_height", 4)?
|
|
||||||
.set_default("show_help", true)?
|
|
||||||
.set_default("invert", false)?
|
|
||||||
.set_default("exit_mode", "return-original")?
|
.set_default("exit_mode", "return-original")?
|
||||||
.set_default("word_jump_mode", "emacs")?
|
.set_default("word_jump_mode", "emacs")?
|
||||||
.set_default(
|
.set_default(
|
||||||
@ -725,13 +335,6 @@ impl Settings {
|
|||||||
.set_default("keymap_mode", "emacs")?
|
.set_default("keymap_mode", "emacs")?
|
||||||
.set_default("keymap_mode_shell", "auto")?
|
.set_default("keymap_mode_shell", "auto")?
|
||||||
.set_default("keymap_cursor", HashMap::<String, String>::new())?
|
.set_default("keymap_cursor", HashMap::<String, String>::new())?
|
||||||
.set_default(
|
|
||||||
"prefers_reduced_motion",
|
|
||||||
std::env::var("NO_MOTION")
|
|
||||||
.ok()
|
|
||||||
.map(|_| config::Value::new(None, config::ValueKind::Boolean(true)))
|
|
||||||
.unwrap_or_else(|| config::Value::new(None, config::ValueKind::Boolean(false))),
|
|
||||||
)?
|
|
||||||
.add_source(
|
.add_source(
|
||||||
Environment::with_prefix("atuin")
|
Environment::with_prefix("atuin")
|
||||||
.prefix_separator("_")
|
.prefix_separator("_")
|
||||||
@ -819,42 +422,3 @@ impl Default for Settings {
|
|||||||
.expect("Could not deserialize config")
|
.expect("Could not deserialize config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use eyre::Result;
|
|
||||||
|
|
||||||
use super::Timezone;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_parse_offset_timezone_spec() -> Result<()> {
|
|
||||||
assert_eq!(Timezone::from_str("+02")?.0.as_hms(), (2, 0, 0));
|
|
||||||
assert_eq!(Timezone::from_str("-04")?.0.as_hms(), (-4, 0, 0));
|
|
||||||
assert_eq!(Timezone::from_str("+05:30")?.0.as_hms(), (5, 30, 0));
|
|
||||||
assert_eq!(Timezone::from_str("-09:30")?.0.as_hms(), (-9, -30, 0));
|
|
||||||
|
|
||||||
// single digit hours are allowed
|
|
||||||
assert_eq!(Timezone::from_str("+2")?.0.as_hms(), (2, 0, 0));
|
|
||||||
assert_eq!(Timezone::from_str("-4")?.0.as_hms(), (-4, 0, 0));
|
|
||||||
assert_eq!(Timezone::from_str("+5:30")?.0.as_hms(), (5, 30, 0));
|
|
||||||
assert_eq!(Timezone::from_str("-9:30")?.0.as_hms(), (-9, -30, 0));
|
|
||||||
|
|
||||||
// fully qualified form
|
|
||||||
assert_eq!(Timezone::from_str("+09:30:00")?.0.as_hms(), (9, 30, 0));
|
|
||||||
assert_eq!(Timezone::from_str("-09:30:00")?.0.as_hms(), (-9, -30, 0));
|
|
||||||
|
|
||||||
// these offsets don't really exist but are supported anyway
|
|
||||||
assert_eq!(Timezone::from_str("+0:5")?.0.as_hms(), (0, 5, 0));
|
|
||||||
assert_eq!(Timezone::from_str("-0:5")?.0.as_hms(), (0, -5, 0));
|
|
||||||
assert_eq!(Timezone::from_str("+01:23:45")?.0.as_hms(), (1, 23, 45));
|
|
||||||
assert_eq!(Timezone::from_str("-01:23:45")?.0.as_hms(), (-1, -23, -45));
|
|
||||||
|
|
||||||
// require a leading sign for clarity
|
|
||||||
assert!(Timezone::from_str("5").is_err());
|
|
||||||
assert!(Timezone::from_str("10:30").is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
86
atuin-client/src/settings/behaviour.rs
Normal file
86
atuin-client/src/settings/behaviour.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::Settings;
|
||||||
|
|
||||||
|
// Exit
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy)]
|
||||||
|
pub enum ExitMode {
|
||||||
|
#[serde(rename = "return-original")]
|
||||||
|
ReturnOriginal,
|
||||||
|
|
||||||
|
#[serde(rename = "return-query")]
|
||||||
|
ReturnQuery,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum FilterMode {
|
||||||
|
#[serde(rename = "global")]
|
||||||
|
Global = 0,
|
||||||
|
|
||||||
|
#[serde(rename = "host")]
|
||||||
|
Host = 1,
|
||||||
|
|
||||||
|
#[serde(rename = "session")]
|
||||||
|
Session = 2,
|
||||||
|
|
||||||
|
#[serde(rename = "directory")]
|
||||||
|
Directory = 3,
|
||||||
|
|
||||||
|
#[serde(rename = "workspace")]
|
||||||
|
Workspace = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterMode {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
FilterMode::Global => "GLOBAL",
|
||||||
|
FilterMode::Host => "HOST",
|
||||||
|
FilterMode::Session => "SESSION",
|
||||||
|
FilterMode::Directory => "DIRECTORY",
|
||||||
|
FilterMode::Workspace => "WORKSPACE",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy, ValueEnum, PartialEq)]
|
||||||
|
pub enum SearchMode {
|
||||||
|
#[serde(rename = "prefix")]
|
||||||
|
Prefix,
|
||||||
|
|
||||||
|
#[serde(rename = "fulltext")]
|
||||||
|
#[clap(aliases = &["fulltext"])]
|
||||||
|
FullText,
|
||||||
|
|
||||||
|
#[serde(rename = "fuzzy")]
|
||||||
|
Fuzzy,
|
||||||
|
|
||||||
|
#[serde(rename = "skim")]
|
||||||
|
Skim,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchMode {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SearchMode::Prefix => "PREFIX",
|
||||||
|
SearchMode::FullText => "FULLTXT",
|
||||||
|
SearchMode::Fuzzy => "FUZZY",
|
||||||
|
SearchMode::Skim => "SKIM",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn next(&self, settings: &Settings) -> Self {
|
||||||
|
match self {
|
||||||
|
SearchMode::Prefix => SearchMode::FullText,
|
||||||
|
// if the user is using skim, we go to skim
|
||||||
|
SearchMode::FullText if settings.search_mode == SearchMode::Skim => SearchMode::Skim,
|
||||||
|
// otherwise fuzzy.
|
||||||
|
SearchMode::FullText => SearchMode::Fuzzy,
|
||||||
|
SearchMode::Fuzzy | SearchMode::Skim => SearchMode::Prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
161
atuin-client/src/settings/display.rs
Normal file
161
atuin-client/src/settings/display.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use config::{builder::DefaultState, ConfigBuilder, Value, ValueKind};
|
||||||
|
use eyre::Result;
|
||||||
|
use ratatui::style::{Color, Style, Stylize};
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub inline_height: u16,
|
||||||
|
pub invert: bool,
|
||||||
|
pub max_preview_height: u16,
|
||||||
|
pub prefers_reduced_motion: bool,
|
||||||
|
pub show_preview: bool,
|
||||||
|
pub show_help: bool,
|
||||||
|
#[serde(alias = "display")]
|
||||||
|
pub style: Display,
|
||||||
|
#[serde(default)]
|
||||||
|
pub styles: Styles,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
|
||||||
|
pub(crate) fn defaults(
|
||||||
|
builder: ConfigBuilder<DefaultState>,
|
||||||
|
) -> Result<ConfigBuilder<DefaultState>> {
|
||||||
|
Ok(builder
|
||||||
|
.set_default("inline_height", 0)?
|
||||||
|
.set_default("invert", false)?
|
||||||
|
.set_default("max_preview_height", 4)?
|
||||||
|
.set_default(
|
||||||
|
"prefers_reduced_motion",
|
||||||
|
env::var("NO_MOTION")
|
||||||
|
.ok()
|
||||||
|
.map(|_| Value::new(None, ValueKind::Boolean(true)))
|
||||||
|
.unwrap_or_else(|| Value::new(None, ValueKind::Boolean(false))),
|
||||||
|
)?
|
||||||
|
.set_default("show_preview", false)?
|
||||||
|
.set_default("show_help", true)?
|
||||||
|
.set_default("style", "auto")?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display (previously Style, still "style" as a configuration value, with an
|
||||||
|
// optional alias of "display" - potentially deprecate "style" in future)
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy)]
|
||||||
|
pub enum Display {
|
||||||
|
#[serde(rename = "auto")]
|
||||||
|
Auto,
|
||||||
|
|
||||||
|
#[serde(rename = "full")]
|
||||||
|
Full,
|
||||||
|
|
||||||
|
#[serde(rename = "compact")]
|
||||||
|
Compact,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
pub struct Styles {
|
||||||
|
#[serde(default, deserialize_with = "Variants::deserialize_style")]
|
||||||
|
pub command: Option<Style>,
|
||||||
|
#[serde(default, deserialize_with = "Variants::deserialize_style")]
|
||||||
|
pub command_selected: Option<Style>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum Variants {
|
||||||
|
Color(Color),
|
||||||
|
Components(Components),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Variants {
|
||||||
|
fn deserialize_style<'de, D>(deserializer: D) -> Result<Option<Style>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let variants: Option<Variants> = Deserialize::deserialize(deserializer)?;
|
||||||
|
let style: Option<Style> = variants.map(|variants| variants.into());
|
||||||
|
|
||||||
|
Ok(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Variants> for Style {
|
||||||
|
fn from(value: Variants) -> Style {
|
||||||
|
match value {
|
||||||
|
Variants::Components(complex_style) => complex_style.into(),
|
||||||
|
Variants::Color(color) => color.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
struct Components {
|
||||||
|
// Colors
|
||||||
|
#[serde(default)]
|
||||||
|
pub foreground: Option<Color>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub background: Option<Color>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub underline: Option<Color>,
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
#[serde(default)]
|
||||||
|
pub bold: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub crossed_out: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub italic: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub underlined: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Components> for Style {
|
||||||
|
fn from(value: Components) -> Style {
|
||||||
|
let mut style = Style::default();
|
||||||
|
|
||||||
|
if let Some(color) = value.foreground {
|
||||||
|
style = style.fg(color);
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(color) = value.background {
|
||||||
|
style = style.bg(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(color) = value.underline {
|
||||||
|
style = style.underline_color(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
style = match value.bold {
|
||||||
|
Some(true) => style.bold(),
|
||||||
|
Some(_) => style.not_bold(),
|
||||||
|
_ => style,
|
||||||
|
};
|
||||||
|
|
||||||
|
style = match value.crossed_out {
|
||||||
|
Some(true) => style.crossed_out(),
|
||||||
|
Some(_) => style.not_crossed_out(),
|
||||||
|
_ => style,
|
||||||
|
};
|
||||||
|
|
||||||
|
style = match value.italic {
|
||||||
|
Some(true) => style.italic(),
|
||||||
|
Some(_) => style.not_italic(),
|
||||||
|
_ => style,
|
||||||
|
};
|
||||||
|
|
||||||
|
style = match value.underlined {
|
||||||
|
Some(true) => style.underlined(),
|
||||||
|
Some(_) => style.not_underlined(),
|
||||||
|
_ => style,
|
||||||
|
};
|
||||||
|
|
||||||
|
style
|
||||||
|
}
|
||||||
|
}
|
85
atuin-client/src/settings/input.rs
Normal file
85
atuin-client/src/settings/input.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
use clap::ValueEnum;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum KeymapMode {
|
||||||
|
#[serde(rename = "emacs")]
|
||||||
|
Emacs,
|
||||||
|
|
||||||
|
#[serde(rename = "vim-normal")]
|
||||||
|
VimNormal,
|
||||||
|
|
||||||
|
#[serde(rename = "vim-insert")]
|
||||||
|
VimInsert,
|
||||||
|
|
||||||
|
#[serde(rename = "auto")]
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapMode {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
KeymapMode::Emacs => "EMACS",
|
||||||
|
KeymapMode::VimNormal => "VIMNORMAL",
|
||||||
|
KeymapMode::VimInsert => "VIMINSERT",
|
||||||
|
KeymapMode::Auto => "AUTO",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to translate the config to crossterm::cursor::SetCursorStyle, but
|
||||||
|
// the original type does not implement trait serde::Deserialize unfortunately.
|
||||||
|
// It seems impossible to implement Deserialize for external types when it is
|
||||||
|
// used in HashMap (https://stackoverflow.com/questions/67142663). We instead
|
||||||
|
// define an adapter type.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy, PartialEq, Eq, ValueEnum)]
|
||||||
|
pub enum CursorStyle {
|
||||||
|
#[serde(rename = "default")]
|
||||||
|
DefaultUserShape,
|
||||||
|
|
||||||
|
#[serde(rename = "blink-block")]
|
||||||
|
BlinkingBlock,
|
||||||
|
|
||||||
|
#[serde(rename = "steady-block")]
|
||||||
|
SteadyBlock,
|
||||||
|
|
||||||
|
#[serde(rename = "blink-underline")]
|
||||||
|
BlinkingUnderScore,
|
||||||
|
|
||||||
|
#[serde(rename = "steady-underline")]
|
||||||
|
SteadyUnderScore,
|
||||||
|
|
||||||
|
#[serde(rename = "blink-bar")]
|
||||||
|
BlinkingBar,
|
||||||
|
|
||||||
|
#[serde(rename = "steady-bar")]
|
||||||
|
SteadyBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CursorStyle {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
CursorStyle::DefaultUserShape => "DEFAULT",
|
||||||
|
CursorStyle::BlinkingBlock => "BLINKBLOCK",
|
||||||
|
CursorStyle::SteadyBlock => "STEADYBLOCK",
|
||||||
|
CursorStyle::BlinkingUnderScore => "BLINKUNDERLINE",
|
||||||
|
CursorStyle::SteadyUnderScore => "STEADYUNDERLINE",
|
||||||
|
CursorStyle::BlinkingBar => "BLINKBAR",
|
||||||
|
CursorStyle::SteadyBar => "STEADYBAR",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
pub struct Keys {
|
||||||
|
pub scroll_exits: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy)]
|
||||||
|
pub enum WordJumpMode {
|
||||||
|
#[serde(rename = "emacs")]
|
||||||
|
Emacs,
|
||||||
|
|
||||||
|
#[serde(rename = "subl")]
|
||||||
|
Subl,
|
||||||
|
}
|
78
atuin-client/src/settings/stats.rs
Normal file
78
atuin-client/src/settings/stats.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Stats {
|
||||||
|
#[serde(default = "Stats::common_prefix_default")]
|
||||||
|
pub common_prefix: Vec<String>, // sudo, etc. commands we want to strip off
|
||||||
|
#[serde(default = "Stats::common_subcommands_default")]
|
||||||
|
pub common_subcommands: Vec<String>, // kubectl, commands we should consider subcommands for
|
||||||
|
#[serde(default = "Stats::ignored_commands_default")]
|
||||||
|
pub ignored_commands: Vec<String>, // cd, ls, etc. commands we want to completely hide from stats
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stats {
|
||||||
|
fn common_prefix_default() -> Vec<String> {
|
||||||
|
vec!["sudo", "doas"].into_iter().map(String::from).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_subcommands_default() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"apt",
|
||||||
|
"cargo",
|
||||||
|
"composer",
|
||||||
|
"dnf",
|
||||||
|
"docker",
|
||||||
|
"git",
|
||||||
|
"go",
|
||||||
|
"ip",
|
||||||
|
"kubectl",
|
||||||
|
"nix",
|
||||||
|
"nmcli",
|
||||||
|
"npm",
|
||||||
|
"pecl",
|
||||||
|
"pnpm",
|
||||||
|
"podman",
|
||||||
|
"port",
|
||||||
|
"systemctl",
|
||||||
|
"tmux",
|
||||||
|
"yarn",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ignored_commands_default() -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Stats {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
common_prefix: Self::common_prefix_default(),
|
||||||
|
common_subcommands: Self::common_subcommands_default(),
|
||||||
|
ignored_commands: Self::ignored_commands_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Can use upstream Dialect enum if https://github.com/stevedonovan/chrono-english/pull/16 is merged
|
||||||
|
// FIXME: Above PR was merged, but dependency was changed to interim (fork of chrono-english) in the ... interim
|
||||||
|
#[derive(Clone, Debug, Deserialize, Copy)]
|
||||||
|
pub enum Dialect {
|
||||||
|
#[serde(rename = "us")]
|
||||||
|
Us,
|
||||||
|
|
||||||
|
#[serde(rename = "uk")]
|
||||||
|
Uk,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Dialect> for interim::Dialect {
|
||||||
|
fn from(d: Dialect) -> interim::Dialect {
|
||||||
|
match d {
|
||||||
|
Dialect::Uk => interim::Dialect::Uk,
|
||||||
|
Dialect::Us => interim::Dialect::Us,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
atuin-client/src/settings/sync.rs
Normal file
6
atuin-client/src/settings/sync.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Default)]
|
||||||
|
pub struct Sync {
|
||||||
|
pub records: bool,
|
||||||
|
}
|
91
atuin-client/src/settings/time.rs
Normal file
91
atuin-client/src/settings/time.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
use eyre::{bail, Error, Result};
|
||||||
|
use serde_with::DeserializeFromStr;
|
||||||
|
use time::{format_description::FormatItem, macros::format_description, UtcOffset};
|
||||||
|
|
||||||
|
/// format: <+|-><hour>[:<minute>[:<second>]]
|
||||||
|
static OFFSET_FMT: &[FormatItem<'_>] =
|
||||||
|
format_description!("[offset_hour sign:mandatory padding:none][optional [:[offset_minute padding:none][optional [:[offset_second padding:none]]]]]");
|
||||||
|
|
||||||
|
/// Type wrapper around `time::UtcOffset` to support a wider variety of timezone formats.
|
||||||
|
///
|
||||||
|
/// Note that the parsing of this struct needs to be done before starting any
|
||||||
|
/// multithreaded runtime, otherwise it will fail on most Unix systems.
|
||||||
|
///
|
||||||
|
/// See: https://github.com/atuinsh/atuin/pull/1517#discussion_r1447516426
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, DeserializeFromStr)]
|
||||||
|
pub struct Timezone(pub UtcOffset);
|
||||||
|
impl fmt::Display for Timezone {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Timezone {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
// local timezone
|
||||||
|
if matches!(s.to_lowercase().as_str(), "l" | "local") {
|
||||||
|
let offset = UtcOffset::current_local_offset()?;
|
||||||
|
return Ok(Self(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(s.to_lowercase().as_str(), "0" | "utc") {
|
||||||
|
let offset = UtcOffset::UTC;
|
||||||
|
return Ok(Self(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// offset from UTC
|
||||||
|
if let Ok(offset) = UtcOffset::parse(s, OFFSET_FMT) {
|
||||||
|
return Ok(Self(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDEA: Currently named timezones are not supported, because the well-known crate
|
||||||
|
// for this is `chrono_tz`, which is not really interoperable with the datetime crate
|
||||||
|
// that we currently use - `time`. If ever we migrate to using `chrono`, this would
|
||||||
|
// be a good feature to add.
|
||||||
|
|
||||||
|
bail!(r#""{s}" is not a valid timezone spec"#)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use eyre::Result;
|
||||||
|
|
||||||
|
use super::Timezone;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_parse_offset_timezone_spec() -> Result<()> {
|
||||||
|
assert_eq!(Timezone::from_str("+02")?.0.as_hms(), (2, 0, 0));
|
||||||
|
assert_eq!(Timezone::from_str("-04")?.0.as_hms(), (-4, 0, 0));
|
||||||
|
assert_eq!(Timezone::from_str("+05:30")?.0.as_hms(), (5, 30, 0));
|
||||||
|
assert_eq!(Timezone::from_str("-09:30")?.0.as_hms(), (-9, -30, 0));
|
||||||
|
|
||||||
|
// single digit hours are allowed
|
||||||
|
assert_eq!(Timezone::from_str("+2")?.0.as_hms(), (2, 0, 0));
|
||||||
|
assert_eq!(Timezone::from_str("-4")?.0.as_hms(), (-4, 0, 0));
|
||||||
|
assert_eq!(Timezone::from_str("+5:30")?.0.as_hms(), (5, 30, 0));
|
||||||
|
assert_eq!(Timezone::from_str("-9:30")?.0.as_hms(), (-9, -30, 0));
|
||||||
|
|
||||||
|
// fully qualified form
|
||||||
|
assert_eq!(Timezone::from_str("+09:30:00")?.0.as_hms(), (9, 30, 0));
|
||||||
|
assert_eq!(Timezone::from_str("-09:30:00")?.0.as_hms(), (-9, -30, 0));
|
||||||
|
|
||||||
|
// these offsets don't really exist but are supported anyway
|
||||||
|
assert_eq!(Timezone::from_str("+0:5")?.0.as_hms(), (0, 5, 0));
|
||||||
|
assert_eq!(Timezone::from_str("-0:5")?.0.as_hms(), (0, -5, 0));
|
||||||
|
assert_eq!(Timezone::from_str("+01:23:45")?.0.as_hms(), (1, 23, 45));
|
||||||
|
assert_eq!(Timezone::from_str("-01:23:45")?.0.as_hms(), (-1, -23, -45));
|
||||||
|
|
||||||
|
// require a leading sign for clarity
|
||||||
|
assert!(Timezone::from_str("5").is_err());
|
||||||
|
assert!(Timezone::from_str("10:30").is_err());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -158,7 +158,7 @@ impl Cmd {
|
|||||||
settings.filter_mode = self.filter_mode.unwrap();
|
settings.filter_mode = self.filter_mode.unwrap();
|
||||||
}
|
}
|
||||||
if self.inline_height.is_some() {
|
if self.inline_height.is_some() {
|
||||||
settings.inline_height = self.inline_height.unwrap();
|
settings.display.inline_height = self.inline_height.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.shell_up_key_binding = self.shell_up_key_binding;
|
settings.shell_up_key_binding = self.shell_up_key_binding;
|
||||||
|
@ -22,7 +22,9 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
use atuin_client::{
|
use atuin_client::{
|
||||||
database::{current_context, Database},
|
database::{current_context, Database},
|
||||||
history::{store::HistoryStore, History, HistoryStats},
|
history::{store::HistoryStore, History, HistoryStats},
|
||||||
settings::{CursorStyle, ExitMode, FilterMode, KeymapMode, SearchMode, Settings, Styles},
|
settings::{
|
||||||
|
CursorStyle, Display, ExitMode, FilterMode, KeymapMode, SearchMode, Settings, Styles,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -240,11 +242,11 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_search_up(&mut self, settings: &Settings, enable_exit: bool) -> InputAction {
|
fn handle_search_up(&mut self, settings: &Settings, enable_exit: bool) -> InputAction {
|
||||||
self.handle_search_scroll_one_line(settings, enable_exit, settings.invert)
|
self.handle_search_scroll_one_line(settings, enable_exit, settings.display.invert)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_search_down(&mut self, settings: &Settings, enable_exit: bool) -> InputAction {
|
fn handle_search_down(&mut self, settings: &Settings, enable_exit: bool) -> InputAction {
|
||||||
self.handle_search_scroll_one_line(settings, enable_exit, !settings.invert)
|
self.handle_search_scroll_one_line(settings, enable_exit, !settings.display.invert)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_search_accept(&mut self, settings: &Settings) -> InputAction {
|
fn handle_search_accept(&mut self, settings: &Settings) -> InputAction {
|
||||||
@ -450,19 +452,19 @@ impl State {
|
|||||||
KeyCode::Char(c) => {
|
KeyCode::Char(c) => {
|
||||||
self.search.input.insert(c);
|
self.search.input.insert(c);
|
||||||
}
|
}
|
||||||
KeyCode::PageDown if !settings.invert => {
|
KeyCode::PageDown if !settings.display.invert => {
|
||||||
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
||||||
self.scroll_down(scroll_len);
|
self.scroll_down(scroll_len);
|
||||||
}
|
}
|
||||||
KeyCode::PageDown if settings.invert => {
|
KeyCode::PageDown if settings.display.invert => {
|
||||||
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
||||||
self.scroll_up(scroll_len);
|
self.scroll_up(scroll_len);
|
||||||
}
|
}
|
||||||
KeyCode::PageUp if !settings.invert => {
|
KeyCode::PageUp if !settings.display.invert => {
|
||||||
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
||||||
self.scroll_up(scroll_len);
|
self.scroll_up(scroll_len);
|
||||||
}
|
}
|
||||||
KeyCode::PageUp if settings.invert => {
|
KeyCode::PageUp if settings.display.invert => {
|
||||||
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
let scroll_len = self.results_state.max_entries() - settings.scroll_context_lines;
|
||||||
self.scroll_down(scroll_len);
|
self.scroll_down(scroll_len);
|
||||||
}
|
}
|
||||||
@ -492,21 +494,21 @@ impl State {
|
|||||||
stats: Option<HistoryStats>,
|
stats: Option<HistoryStats>,
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
) {
|
) {
|
||||||
let compact = match settings.style {
|
let compact = match settings.display.style {
|
||||||
atuin_client::settings::Style::Auto => f.size().height < 14,
|
Display::Auto => f.size().height < 14,
|
||||||
atuin_client::settings::Style::Compact => true,
|
Display::Compact => true,
|
||||||
atuin_client::settings::Style::Full => false,
|
Display::Full => false,
|
||||||
};
|
};
|
||||||
let invert = settings.invert;
|
let invert = settings.display.invert;
|
||||||
let border_size = if compact { 0 } else { 1 };
|
let border_size = if compact { 0 } else { 1 };
|
||||||
let preview_width = f.size().width - 2;
|
let preview_width = f.size().width - 2;
|
||||||
let preview_height = if settings.show_preview && self.tab_index == 0 {
|
let preview_height = if settings.display.show_preview && self.tab_index == 0 {
|
||||||
let longest_command = results
|
let longest_command = results
|
||||||
.iter()
|
.iter()
|
||||||
.max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
|
.max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
|
||||||
longest_command.map_or(0, |v| {
|
longest_command.map_or(0, |v| {
|
||||||
std::cmp::min(
|
std::cmp::min(
|
||||||
settings.max_preview_height,
|
settings.display.max_preview_height,
|
||||||
v.command
|
v.command
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(|line| {
|
.map(|line| {
|
||||||
@ -521,7 +523,7 @@ impl State {
|
|||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
};
|
};
|
||||||
let show_help = settings.show_help && (!compact || f.size().height > 1);
|
let show_help = settings.display.show_help && (!compact || f.size().height > 1);
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(0)
|
.margin(0)
|
||||||
@ -601,7 +603,7 @@ impl State {
|
|||||||
results,
|
results,
|
||||||
self.keymap_mode,
|
self.keymap_mode,
|
||||||
&self.now,
|
&self.now,
|
||||||
&settings.styles,
|
&settings.display.styles,
|
||||||
);
|
);
|
||||||
f.render_stateful_widget(results_list, results_list_chunk, &mut self.results_state);
|
f.render_stateful_widget(results_list, results_list_chunk, &mut self.results_state);
|
||||||
}
|
}
|
||||||
@ -896,13 +898,13 @@ pub async fn history(
|
|||||||
mut db: impl Database,
|
mut db: impl Database,
|
||||||
history_store: &HistoryStore,
|
history_store: &HistoryStore,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let stdout = Stdout::new(settings.inline_height > 0)?;
|
let stdout = Stdout::new(settings.display.inline_height > 0)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal = Terminal::with_options(
|
let mut terminal = Terminal::with_options(
|
||||||
backend,
|
backend,
|
||||||
TerminalOptions {
|
TerminalOptions {
|
||||||
viewport: if settings.inline_height > 0 {
|
viewport: if settings.display.inline_height > 0 {
|
||||||
Viewport::Inline(settings.inline_height)
|
Viewport::Inline(settings.display.inline_height)
|
||||||
} else {
|
} else {
|
||||||
Viewport::Fullscreen
|
Viewport::Fullscreen
|
||||||
},
|
},
|
||||||
@ -955,7 +957,7 @@ pub async fn history(
|
|||||||
value => value,
|
value => value,
|
||||||
},
|
},
|
||||||
current_cursor: None,
|
current_cursor: None,
|
||||||
now: if settings.prefers_reduced_motion {
|
now: if settings.display.prefers_reduced_motion {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
Box::new(move || now)
|
Box::new(move || now)
|
||||||
} else {
|
} else {
|
||||||
@ -1041,7 +1043,7 @@ pub async fn history(
|
|||||||
|
|
||||||
app.finalize_keymap_cursor(settings);
|
app.finalize_keymap_cursor(settings);
|
||||||
|
|
||||||
if settings.inline_height > 0 {
|
if settings.display.inline_height > 0 {
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user