aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock8
-rw-r--r--Cargo.toml1
-rw-r--r--atuin-client/Cargo.toml1
-rw-r--r--atuin-client/config.toml3
-rw-r--r--atuin-client/src/api_client.rs26
-rw-r--r--atuin-client/src/settings.rs121
-rw-r--r--atuin-common/src/api.rs6
-rw-r--r--atuin-server/src/handlers/mod.rs9
-rw-r--r--docs/config.md9
-rw-r--r--src/command/client/history.rs1
-rw-r--r--src/command/client/search.rs9
-rw-r--r--src/command/client/search/interactive.rs37
-rw-r--r--src/main.rs3
13 files changed, 191 insertions, 43 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5bd71fe4..a1307076 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -100,6 +100,7 @@ dependencies = [
"log",
"pretty_env_logger",
"rpassword",
+ "semver",
"serde",
"serde_json",
"termion",
@@ -133,6 +134,7 @@ dependencies = [
"regex",
"reqwest",
"rmp-serde",
+ "semver",
"serde",
"serde_json",
"sha2",
@@ -1636,6 +1638,12 @@ dependencies = [
]
[[package]]
+name = "semver"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+
+[[package]]
name = "serde"
version = "1.0.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 70fcd1d0..71af5d44 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -71,6 +71,7 @@ clap_complete = "3.1.4"
fs-err = "2.7"
whoami = "1.1.2"
rpassword = "6.0"
+semver = "1.0.14"
[dependencies.tracing-subscriber]
version = "0.3"
diff --git a/atuin-client/Cargo.toml b/atuin-client/Cargo.toml
index 7441640c..dff89315 100644
--- a/atuin-client/Cargo.toml
+++ b/atuin-client/Cargo.toml
@@ -63,6 +63,7 @@ sha2 = { version = "0.10", optional = true }
rmp-serde = { version = "1.0.0", optional = true }
base64 = { version = "0.13.0", optional = true }
tokio = { version = "1", features = ["full"] }
+semver = "1.0.14"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
diff --git a/atuin-client/config.toml b/atuin-client/config.toml
index 0d82ac28..43b5e24c 100644
--- a/atuin-client/config.toml
+++ b/atuin-client/config.toml
@@ -15,6 +15,9 @@
## enable or disable automatic sync
# auto_sync = true
+## enable or disable automatic update checks
+# update_check = true
+
## how often to sync history. note that this is only triggered when a command
## is ran, so sync intervals may well be longer
## set it to 0 to sync after every command
diff --git a/atuin-client/src/api_client.rs b/atuin-client/src/api_client.rs
index 5692fea0..b20d9378 100644
--- a/atuin-client/src/api_client.rs
+++ b/atuin-client/src/api_client.rs
@@ -9,9 +9,10 @@ use reqwest::{
use sodiumoxide::crypto::secretbox;
use atuin_common::api::{
- AddHistoryRequest, CountResponse, ErrorResponse, LoginRequest, LoginResponse, RegisterResponse,
- SyncHistoryResponse,
+ AddHistoryRequest, CountResponse, ErrorResponse, IndexResponse, LoginRequest, LoginResponse,
+ RegisterResponse, SyncHistoryResponse,
};
+use semver::Version;
use crate::{
encryption::{decode_key, decrypt},
@@ -86,6 +87,27 @@ pub async fn login(address: &str, req: LoginRequest) -> Result<LoginResponse> {
Ok(session)
}
+pub async fn latest_version() -> Result<Version> {
+ let url = "https://api.atuin.sh";
+ let client = reqwest::Client::new();
+
+ let resp = client
+ .get(url)
+ .header(USER_AGENT, APP_USER_AGENT)
+ .send()
+ .await?;
+
+ if resp.status() != reqwest::StatusCode::OK {
+ let error = resp.json::<ErrorResponse>().await?;
+ bail!("failed to check latest version: {}", error.reason);
+ }
+
+ let index = resp.json::<IndexResponse>().await?;
+ let version = Version::parse(index.version.as_str())?;
+
+ Ok(version)
+}
+
impl<'a> Client<'a> {
pub fn new(sync_addr: &'a str, session_token: &'a str, key: String) -> Result<Self> {
let mut headers = HeaderMap::new();
diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs
index f836ce02..b743a154 100644
--- a/atuin-client/src/settings.rs
+++ b/atuin-client/src/settings.rs
@@ -8,9 +8,13 @@ use config::{Config, Environment, File as ConfigFile, FileFormat};
use eyre::{eyre, Context, Result};
use fs_err::{create_dir_all, File};
use parse_duration::parse;
+use semver::Version;
use serde::Deserialize;
pub const HISTORY_PAGE_SIZE: i64 = 100;
+pub const LAST_SYNC_FILENAME: &str = "last_sync_time";
+pub const LAST_VERSION_CHECK_FILENAME: &str = "last_version_check_time";
+pub const LATEST_VERSION_FILENAME: &str = "latest_version";
#[derive(Clone, Debug, Deserialize, Copy)]
pub enum SearchMode {
@@ -86,6 +90,7 @@ pub struct Settings {
pub dialect: Dialect,
pub style: Style,
pub auto_sync: bool,
+ pub update_check: bool,
pub sync_address: String,
pub sync_frequency: String,
pub db_path: String,
@@ -99,31 +104,65 @@ pub struct Settings {
}
impl Settings {
- pub fn save_sync_time() -> Result<()> {
+ fn save_to_data_dir(filename: &str, value: &str) -> Result<()> {
let data_dir = atuin_common::utils::data_dir();
let data_dir = data_dir.as_path();
- let sync_time_path = data_dir.join("last_sync_time");
+ let path = data_dir.join(filename);
- fs_err::write(sync_time_path, Utc::now().to_rfc3339())?;
+ fs_err::write(path, value)?;
Ok(())
}
- pub fn last_sync() -> Result<chrono::DateTime<Utc>> {
+ fn read_from_data_dir(filename: &str) -> Option<String> {
let data_dir = atuin_common::utils::data_dir();
let data_dir = data_dir.as_path();
- let sync_time_path = data_dir.join("last_sync_time");
+ let path = data_dir.join(filename);
- if !sync_time_path.exists() {
- return Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0));
+ if !path.exists() {
+ return None;
}
- let time = fs_err::read_to_string(sync_time_path)?;
- let time = chrono::DateTime::parse_from_rfc3339(time.as_str())?;
+ let value = fs_err::read_to_string(path);
+
+ value.ok()
+ }
+
+ fn save_current_time(filename: &str) -> Result<()> {
+ Settings::save_to_data_dir(filename, Utc::now().to_rfc3339().as_str())?;
+
+ Ok(())
+ }
+
+ fn load_time_from_file(filename: &str) -> Result<chrono::DateTime<Utc>> {
+ let value = Settings::read_from_data_dir(filename);
- Ok(time.with_timezone(&Utc))
+ match value {
+ Some(v) => {
+ let time = chrono::DateTime::parse_from_rfc3339(v.as_str())?;
+
+ Ok(time.with_timezone(&Utc))
+ }
+ None => Ok(Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
+ }
+ }
+
+ pub fn save_sync_time() -> Result<()> {
+ Settings::save_current_time(LAST_SYNC_FILENAME)
+ }
+
+ pub fn save_version_check_time() -> Result<()> {
+ Settings::save_current_time(LAST_VERSION_CHECK_FILENAME)
+ }
+
+ pub fn last_sync() -> Result<chrono::DateTime<Utc>> {
+ Settings::load_time_from_file(LAST_SYNC_FILENAME)
+ }
+
+ pub fn last_version_check() -> Result<chrono::DateTime<Utc>> {
+ Settings::load_time_from_file(LAST_VERSION_CHECK_FILENAME)
}
pub fn should_sync(&self) -> Result<bool> {
@@ -142,6 +181,67 @@ impl Settings {
}
}
+ fn needs_update_check(&self) -> Result<bool> {
+ let last_check = Settings::last_version_check()?;
+ let diff = Utc::now() - last_check;
+
+ // Check a max of once per hour
+ Ok(diff.num_hours() >= 1)
+ }
+
+ async fn latest_version(&self) -> Result<Version> {
+ // Default to the current version, and if that doesn't parse, a version so high it's unlikely to ever
+ // suggest upgrading.
+ let current =
+ Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or(Version::new(100000, 0, 0));
+
+ if !self.needs_update_check()? {
+ // Worst case, we don't want Atuin to fail to start because something funky is going on with
+ // version checking.
+ let version = match Settings::read_from_data_dir(LATEST_VERSION_FILENAME) {
+ Some(v) => Version::parse(&v).unwrap_or(current),
+ None => current,
+ };
+
+ return Ok(version);
+ }
+
+ #[cfg(feature = "sync")]
+ let latest = crate::api_client::latest_version().await.unwrap_or(current);
+
+ #[cfg(not(feature = "sync"))]
+ let latest = current;
+
+ Settings::save_version_check_time()?;
+ Settings::save_to_data_dir(LATEST_VERSION_FILENAME, latest.to_string().as_str())?;
+
+ Ok(latest)
+ }
+
+ // Return Some(latest version) if an update is needed. Otherwise, none.
+ pub async fn needs_update(&self) -> Option<Version> {
+ if !self.update_check {
+ return None;
+ }
+
+ let current =
+ Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or(Version::new(100000, 0, 0));
+
+ let latest = self.latest_version().await;
+
+ if latest.is_err() {
+ return None;
+ }
+
+ let latest = latest.unwrap();
+
+ if latest > current {
+ return Some(latest);
+ }
+
+ None
+ }
+
pub fn new() -> Result<Self> {
let config_dir = atuin_common::utils::config_dir();
@@ -172,6 +272,7 @@ impl Settings {
.set_default("session_path", session_path.to_str())?
.set_default("dialect", "us")?
.set_default("auto_sync", true)?
+ .set_default("update_check", true)?
.set_default("sync_frequency", "1h")?
.set_default("sync_address", "https://api.atuin.sh")?
.set_default("search_mode", "prefix")?
diff --git a/atuin-common/src/api.rs b/atuin-common/src/api.rs
index ba04fd78..f17cfd58 100644
--- a/atuin-common/src/api.rs
+++ b/atuin-common/src/api.rs
@@ -59,3 +59,9 @@ pub struct SyncHistoryResponse {
pub struct ErrorResponse<'a> {
pub reason: Cow<'a, str>,
}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct IndexResponse {
+ pub homage: String,
+ pub version: String,
+}
diff --git a/atuin-server/src/handlers/mod.rs b/atuin-server/src/handlers/mod.rs
index 4be3e388..082ae471 100644
--- a/atuin-server/src/handlers/mod.rs
+++ b/atuin-server/src/handlers/mod.rs
@@ -1,18 +1,11 @@
-use atuin_common::api::ErrorResponse;
+use atuin_common::api::{ErrorResponse, IndexResponse};
use axum::{response::IntoResponse, Json};
-use serde::{Deserialize, Serialize};
pub mod history;
pub mod user;
const VERSION: &str = env!("CARGO_PKG_VERSION");
-#[derive(Debug, Serialize, Deserialize)]
-pub struct IndexResponse {
- pub homage: String,
- pub version: String,
-}
-
pub async fn index() -> Json<IndexResponse> {
let homage = r#""Through the fathomless deeps of space swims the star turtle Great A'Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld." -- Sir Terry Pratchett"#;
diff --git a/docs/config.md b/docs/config.md
index 42d8b0bc..13aabac2 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -47,6 +47,15 @@ true
auto_sync = true/false
```
+### `update_check`
+
+Configures whether or not to automatically check for updates. Defaults to
+true.
+
+```
+auto_sync = true/false
+```
+
### `sync_address`
The address of the server to sync with! Defaults to `https://api.atuin.sh`.
diff --git a/src/command/client/history.rs b/src/command/client/history.rs
index fe5bfcbe..07f4b319 100644
--- a/src/command/client/history.rs
+++ b/src/command/client/history.rs
@@ -15,6 +15,7 @@ use atuin_client::{
#[cfg(feature = "sync")]
use atuin_client::sync;
+use log::debug;
use super::search::format_duration;
diff --git a/src/command/client/search.rs b/src/command/client/search.rs
index eda20ac5..1cef1ffc 100644
--- a/src/command/client/search.rs
+++ b/src/command/client/search.rs
@@ -63,14 +63,7 @@ pub struct Cmd {
impl Cmd {
pub async fn run(self, db: &mut impl Database, settings: &Settings) -> Result<()> {
if self.interactive {
- let item = interactive::history(
- &self.query,
- settings.search_mode,
- settings.filter_mode,
- settings.style,
- db,
- )
- .await?;
+ let item = interactive::history(&self.query, settings, db).await?;
eprintln!("{}", item);
} else {
let list_mode = ListMode::from_flags(self.human, self.cmd_only);
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
index 069b7f3c..950a971d 100644
--- a/src/command/client/search/interactive.rs
+++ b/src/command/client/search/interactive.rs
@@ -1,6 +1,7 @@
use std::io::stdout;
use eyre::Result;
+use semver::Version;
use termion::{
event::Event as TermEvent, event::Key, event::MouseButton, event::MouseEvent,
input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen,
@@ -20,7 +21,7 @@ use atuin_client::{
database::Context,
database::Database,
history::History,
- settings::{FilterMode, SearchMode},
+ settings::{FilterMode, SearchMode, Settings},
};
use super::{
@@ -36,6 +37,7 @@ struct State {
filter_mode: FilterMode,
results_state: ListState,
context: Context,
+ update_needed: Option<Version>,
}
impl State {
@@ -143,10 +145,19 @@ impl State {
.constraints([Constraint::Length(1); 3])
.split(top_chunks[1]);
- let title = Paragraph::new(Text::from(Span::styled(
- format!(" Atuin v{VERSION}"),
- Style::default().add_modifier(Modifier::BOLD),
- )));
+ let title = if self.update_needed.is_some() {
+ let version = self.update_needed.clone().unwrap();
+
+ Paragraph::new(Text::from(Span::styled(
+ format!(" Atuin v{VERSION} - UPDATE AVAILABLE {version}"),
+ Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
+ )))
+ } else {
+ Paragraph::new(Text::from(Span::styled(
+ format!(" Atuin v{VERSION}"),
+ Style::default().add_modifier(Modifier::BOLD),
+ )))
+ };
let help = vec![
Span::raw(" Press "),
@@ -277,9 +288,7 @@ impl State {
#[allow(clippy::cast_possible_truncation)]
pub async fn history(
query: &[String],
- search_mode: SearchMode,
- filter_mode: FilterMode,
- style: atuin_client::settings::Style,
+ settings: &Settings,
db: &mut impl Database,
) -> Result<String> {
let stdout = stdout().into_raw_mode()?;
@@ -294,15 +303,19 @@ pub async fn history(
let mut input = Cursor::from(query.join(" "));
// Put the cursor at the end of the query by default
input.end();
+
+ let update_needed = settings.needs_update().await;
+
let mut app = State {
history_count: db.history_count().await?,
input,
results_state: ListState::default(),
context: current_context(),
- filter_mode,
+ filter_mode: settings.filter_mode,
+ update_needed,
};
- let mut results = app.query_results(search_mode, db).await?;
+ let mut results = app.query_results(settings.search_mode, db).await?;
let index = 'render: loop {
let initial_input = app.input.as_str().to_owned();
@@ -323,10 +336,10 @@ pub async fn history(
}
if initial_input != app.input.as_str() || initial_filter_mode != app.filter_mode {
- results = app.query_results(search_mode, db).await?;
+ results = app.query_results(settings.search_mode, db).await?;
}
- let compact = match style {
+ let compact = match settings.style {
atuin_client::settings::Style::Auto => {
terminal.size().map(|size| size.height < 14).unwrap_or(true)
}
diff --git a/src/main.rs b/src/main.rs
index bffb7249..798f7a23 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,9 +4,6 @@
use clap::{AppSettings, Parser};
use eyre::Result;
-#[macro_use]
-extern crate log;
-
use command::AtuinCmd;
mod command;