Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add window with entry details #36

Merged
merged 12 commits into from
Jul 4, 2024
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ It will be much faster the next time as it no longer needs to fetch the entire r

After some time you will see something like this:

![Screenshot of redu showing the contents of a repo with some marks active](screenshot.png)
![Screenshot of redu showing the contents of a repo](screenshot_start.png)

You can navigate using the **arrow keys** or **hjkl**.
Going right enters a directory and going left leaves back to the parent.
Expand All @@ -68,16 +68,32 @@ where it is the biggest.
The bars indicate the relative size of the item compared to everything else
in the current location.

By pressing **Enter** you can make a small window visible that shows some details
about the currently highlighted item:
- The latest snapshot where it has maximum size
- The earliest date and snapshot where this item appears
- The latest date and snapshot where this item appears

![Screenshot of redu showing the contents of a repo with details open](screenshot_details.png)

You can keep navigating with the details window open and it will update as you
browse around.

### Marking files
You can mark files and directories to build up your list of things to exclude.
Keybinds
- **m**: mark selected file/directory
- **u**: unmark selected file/directory
- **c**: clear all marks
- **c**: clear all marks (this will prompt you for confirmation)

The marks are persistent across runs of redu (they are saved in the cache file),
so feel free to mark a few files and just quit and come back later.

The marks are shown with an asterik at the beginning of the line
and you can see how many total marks you have on the bar at the bottom.

![Screenshot of redu showing the contents of a repo with some marks](screenshot_marks.png)

### Generating the excludes
Press **g** to exit redu and generate a list with all of your marks in alphabetic order to stdout.

Expand Down
Binary file removed screenshot.png
Binary file not shown.
Binary file added screenshot_details.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshot_marks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshot_start.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 90 additions & 6 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{collections::HashSet, path::Path};

use camino::{Utf8Path, Utf8PathBuf};
use chrono::DateTime;
use chrono::{DateTime, Utc};
use log::trace;
use rusqlite::{
functions::FunctionFlags, params, types::FromSqlError, Connection,
Expand Down Expand Up @@ -62,9 +62,7 @@ impl Cache {
.query_and_then([], |row|
Ok(Snapshot {
id: row.get("hash")?,
time: DateTime::from_timestamp_micros(row.get("time")?)
.map(Ok)
.unwrap_or(Err(Error::ExhaustedTimestampPrecision))?,
time: timestamp_to_datetime(row.get("time")?)?,
parent: row.get("parent")?,
tree: row.get("tree")?,
paths: serde_json::from_str(row.get_ref("paths")?.as_str()?)?,
Expand Down Expand Up @@ -122,7 +120,7 @@ impl Cache {
/// This returns the children files/directories of the given path.
/// Each entry's size is the largest size of that file/directory across
/// all snapshots.
pub fn get_max_file_sizes(
pub fn get_entries(
&self,
path_id: Option<PathId>,
) -> Result<Vec<Entry>, rusqlite::Error> {
Expand Down Expand Up @@ -166,6 +164,71 @@ impl Cache {
rows.collect()
}

pub fn get_entry_details(
&self,
path_id: PathId,
) -> Result<EntryDetails, Error> {
let aux = |row: &Row| -> Result<EntryDetails, Error> {
Ok(EntryDetails {
max_size: row.get("max_size")?,
max_size_snapshot_hash: row.get("max_size_snapshot_hash")?,
first_seen: timestamp_to_datetime(row.get("first_seen")?)?,
first_seen_snapshot_hash: row
.get("first_seen_snapshot_hash")?,
last_seen: timestamp_to_datetime(row.get("last_seen")?)?,
last_seen_snapshot_hash: row.get("last_seen_snapshot_hash")?,
})
};
let raw_path_id = path_id.0;
let rich_entries_cte = get_tables(&self.conn)?
.iter()
.filter_map(|name| name.strip_prefix("entries_"))
.map(|snapshot_hash| {
format!(
"SELECT \
hash, \
size, \
time \
FROM \"entries_{snapshot_hash}\" \
JOIN paths ON path_id = paths.id \
JOIN snapshots ON hash = '{snapshot_hash}' \
WHERE path_id = {raw_path_id}\n"
)
})
.intersperse(String::from(" UNION ALL "))
.collect::<String>();
let query = format!(
"WITH \
rich_entries AS ({rich_entries_cte}), \
first_seen AS (
SELECT hash, time
FROM rich_entries
ORDER BY time ASC
LIMIT 1), \
last_seen AS (
SELECT hash, time
FROM rich_entries
ORDER BY time DESC
LIMIT 1), \
max_size AS (
SELECT hash, size
FROM rich_entries
ORDER BY size DESC, time DESC
LIMIT 1) \
SELECT \
max_size.size AS max_size, \
max_size.hash AS max_size_snapshot_hash, \
first_seen.time AS first_seen, \
first_seen.hash as first_seen_snapshot_hash, \
last_seen.time AS last_seen, \
last_seen.hash as last_seen_snapshot_hash \
FROM max_size
JOIN first_seen ON 1=1
JOIN last_seen ON 1=1"
);
self.conn.query_row_and_then(&query, [], aux)
}

pub fn save_snapshot(
&mut self,
snapshot: &Snapshot,
Expand All @@ -190,7 +253,7 @@ impl Cache {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
params![
snapshot.id,
snapshot.time.timestamp_micros(),
datetime_to_timestamp(snapshot.time),
snapshot.parent,
snapshot.tree,
snapshot.hostname,
Expand Down Expand Up @@ -341,6 +404,16 @@ pub struct Entry {
pub is_dir: bool,
}

#[derive(Clone, Debug)]
pub struct EntryDetails {
pub max_size: usize,
pub max_size_snapshot_hash: String,
pub first_seen: DateTime<Utc>,
pub first_seen_snapshot_hash: String,
pub last_seen: DateTime<Utc>,
pub last_seen_snapshot_hash: String,
}

////////// Migrations //////////////////////////////////////////////////////////
type VersionId = u64;

Expand Down Expand Up @@ -513,3 +586,14 @@ fn get_tables(conn: &Connection) -> Result<HashSet<String>, rusqlite::Error> {
let names = stmt.query_map([], |row| row.get(0))?;
names.collect()
}

////////// Misc ////////////////////////////////////////////////////////////////
fn timestamp_to_datetime(timestamp: i64) -> Result<DateTime<Utc>, Error> {
DateTime::from_timestamp_micros(timestamp)
.map(Ok)
.unwrap_or(Err(Error::ExhaustedTimestampPrecision))
}

fn datetime_to_timestamp(datetime: DateTime<Utc>) -> i64 {
datetime.timestamp_micros()
}
4 changes: 3 additions & 1 deletion src/cache/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ fn cache_snapshots_entries() {
vec![]
} else {
cache
.get_max_file_sizes(path_id)
.get_entries(path_id)
.unwrap()
.into_iter()
.map(|e| (e.component, e.size, e.is_dir))
Expand Down Expand Up @@ -505,4 +505,6 @@ fn test_migrate_v0_to_v1() {
assert_marks(&cache, &marks);

assert_eq!(determine_version(&cache.conn).unwrap(), Some(1));

cache_snapshots_entries();
}
16 changes: 9 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ use redu::{
};
use scopeguard::defer;
use thiserror::Error;
use util::snapshot_short_id;

use crate::ui::{Action, App, Event};

mod ui;
mod util;

/// This is like ncdu for a restic respository.
///
Expand Down Expand Up @@ -195,9 +197,11 @@ fn main() -> anyhow::Result<()> {
rect.as_size(),
None,
Utf8PathBuf::new(),
cache.get_max_file_sizes(None)?,
cache.get_entries(None)?,
cache.get_marks().unwrap(),
vec![
"Enter".bold(),
":Details ".into(),
"m".bold(),
":Mark ".into(),
"u".bold(),
Expand Down Expand Up @@ -232,13 +236,15 @@ fn main() -> anyhow::Result<()> {
Action::GetParentEntries(path_id) => {
let parent_id = cache.get_parent_id(path_id)?
.expect("The UI requested a GetParentEntries with a path_id that does not exist");
let entries = cache.get_max_file_sizes(parent_id)?;
let entries = cache.get_entries(parent_id)?;
Some(Event::Entries { path_id: parent_id, entries })
}
Action::GetEntries(path_id) => {
let entries = cache.get_max_file_sizes(path_id)?;
let entries = cache.get_entries(path_id)?;
Some(Event::Entries { path_id, entries })
}
Action::GetEntryDetails(path_id) =>
Some(Event::EntryDetails(cache.get_entry_details(path_id)?)),
Action::UpsertMark(path) => {
cache.upsert_mark(&path)?;
Some(Event::Marks(cache.get_marks()?))
Expand Down Expand Up @@ -606,10 +612,6 @@ fn mpb_insert_end(mpb: &MultiProgress, template: &str) -> ProgressBar {
pb
}

fn snapshot_short_id(id: &str) -> String {
id.chars().take(7).collect::<String>()
}

#[derive(Clone)]
struct FixedSizeQueue<T>(Arc<Mutex<Vec<T>>>);

Expand Down
Loading