Skip to content

Commit 0289300

Browse files
authored
Merge pull request #30 from monofox/feature/mnemonics_cache
Feature: Keep mnemonics for tags / projects
2 parents 4cfa8b1 + a8e3f01 commit 0289300

File tree

8 files changed

+661
-93
lines changed

8 files changed

+661
-93
lines changed

Cargo.lock

Lines changed: 210 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ axum = { version = "0.8.1", features = ["multipart"] }
88
serde = { version = "1.0.197", features = ["derive"] }
99
tokio = { version = "1.36.0", features = ["full", "parking_lot", "tracing"] }
1010
tracing = "0.1.40"
11-
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "parking_lot"] }
11+
tracing-subscriber = { version = "0.3.18", features = [
12+
"env-filter",
13+
"parking_lot",
14+
] }
1215
serde_json = "1.0.114"
1316
tera = { version = "1.20.0" }
1417
anyhow = "1.0.80"
@@ -20,6 +23,11 @@ csv = "1.3.1"
2023
indexmap = { version = "2.2.5", features = ["serde"] }
2124
rand = "0.9.0-beta.3"
2225
dotenvy = { version = "0.15.7" }
23-
taskchampion = { version="2.0.3", default-features = false, features = [] }
26+
taskchampion = { version = "2.0.3", default-features = false, features = [] }
2427
serde_path_to_error = "0.1.17"
2528
shell-words = "1.1.0"
29+
directories = "6.0.0"
30+
toml = "0.8.22"
31+
32+
[dev-dependencies]
33+
tempfile = "3.19.1"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ there will be errors, as no checks, and there may not be any error messages in c
242242
- [ ] Usability improvements on a long task list
243243
- [x] Hiding empty columns
244244
- [ ] Temporary highlight last modified row, if visible
245-
- [ ] Make the mnemonics same for tags on refresh
245+
- [x] Make the mnemonics same for tags on refresh
246246
- [ ] Modification
247247
- [ ] Deleting
248248
- [ ] Following Context

src/core/app.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
use std::{env::{self, home_dir}, path::PathBuf, str::FromStr};
1+
use std::{env::{self, home_dir}, fs::create_dir_all, path::PathBuf, str::FromStr, sync::{Arc, Mutex, RwLock}};
2+
use directories::ProjectDirs;
23
use tera::Context;
4+
use tracing::info;
5+
6+
use super::cache::{FileMnemonicsCache, MnemonicsCacheType};
37

48
/// Holds state information and configurations
59
/// required in the API and business logic operations.
@@ -13,15 +17,20 @@ use tera::Context;
1317
/// | TWK_THEME | theme |
1418
/// | DISPLAY_TIME_OF_THE_DAY | display_time_of_the_day |
1519
/// | TASKDATA | task_storage_path |
20+
/// | TWK_CONFIG_FOLDER | app_config_path |
1621
///
17-
#[derive(Clone, Debug)]
22+
#[derive(Clone)]
1823
pub struct AppState {
1924
pub font: Option<String>,
2025
pub fallback_family: String,
2126
pub theme: Option<String>,
2227
pub display_time_of_the_day: i32,
2328
pub task_storage_path: PathBuf,
2429
pub task_hooks_path: Option<PathBuf>,
30+
pub app_config_path: PathBuf,
31+
pub app_cache_path: PathBuf,
32+
pub app_cache: Arc<RwLock<MnemonicsCacheType>>,
33+
// Here must be cache object for mnemonics
2534
}
2635

2736
impl Default for AppState {
@@ -45,13 +54,51 @@ impl Default for AppState {
4554
.expect("Storage path cannot be found");
4655
let task_hooks_path = Some(home_dir.clone().join("hooks"));
4756

57+
let standard_project_dirs = ProjectDirs::from("", "", "Taskwarrior-Web");
58+
59+
let mut app_config_path: Option<PathBuf> = match env::var("TWK_CONFIG_FOLDER") {
60+
Ok(p) => {
61+
let app_config_path: Result<PathBuf, _> = p.try_into();
62+
match app_config_path {
63+
Ok(x) => Some(x),
64+
Err(_) => None
65+
}
66+
},
67+
Err(_) => None,
68+
};
69+
if app_config_path.is_none() && standard_project_dirs.is_some() {
70+
if let Some(ref proj_dirs) = standard_project_dirs {
71+
app_config_path = Some(proj_dirs.config_dir().to_path_buf());
72+
}
73+
}
74+
75+
let app_config_path = app_config_path.expect("Configuration file found");
76+
let app_cache_path = match standard_project_dirs {
77+
Some(p) => Some(p.cache_dir().to_path_buf()),
78+
None => None,
79+
}.expect("Cache folder not usable.");
80+
81+
// initialize cache.
82+
// ensure, the folder exists.
83+
create_dir_all(app_cache_path.as_path()).expect("Cache folder cannot be created.");
84+
let cache_path = app_cache_path.join("mnemonics.cache");
85+
info!("Cache file to store mnemonics is placed at {:?}", &cache_path);
86+
let mut cache = FileMnemonicsCache::new(Arc::new(Mutex::new(cache_path)));
87+
cache.load().map_err(|e| {
88+
tracing::error!("Cannot parse the configuration file, error: {}", e.to_string());
89+
e
90+
}).expect("Configuration file exists, but is not parsable!");
91+
4892
Self {
4993
font: font,
5094
fallback_family: "monospace".to_string(),
5195
theme: theme,
5296
display_time_of_the_day: display_time_of_the_day,
5397
task_storage_path: task_storage_path,
5498
task_hooks_path: task_hooks_path,
99+
app_config_path: app_config_path,
100+
app_cache_path: app_cache_path,
101+
app_cache: Arc::new(RwLock::new(cache)),
55102
}
56103
}
57104
}

src/core/cache.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use anyhow::anyhow;
2+
use serde::{Deserialize, Serialize};
3+
use std::{
4+
collections::HashMap,
5+
fs::File,
6+
io::{Read, Write},
7+
path::PathBuf,
8+
sync::{Arc, Mutex},
9+
};
10+
11+
#[derive(Clone)]
12+
pub enum MnemonicsType {
13+
PROJECT,
14+
TAG,
15+
}
16+
17+
pub trait MnemonicsCache {
18+
fn insert(
19+
&mut self,
20+
mn_type: MnemonicsType,
21+
key: &str,
22+
value: &str,
23+
) -> Result<(), anyhow::Error>;
24+
fn remove(&mut self, mn_type: MnemonicsType, key: &str) -> Result<(), anyhow::Error>;
25+
fn get(&self, mn_type: MnemonicsType, key: &str) -> Option<String>;
26+
fn save(&self) -> Result<(), anyhow::Error>;
27+
}
28+
29+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30+
pub struct MnemonicsTable {
31+
tags: HashMap<String, String>,
32+
projects: HashMap<String, String>,
33+
}
34+
35+
impl MnemonicsTable {
36+
pub fn get(&self, mn_type: MnemonicsType) -> &HashMap<String, String> {
37+
match mn_type {
38+
MnemonicsType::PROJECT => &self.projects,
39+
MnemonicsType::TAG => &self.tags,
40+
}
41+
}
42+
43+
pub fn insert(&mut self, mn_type: MnemonicsType, key: &str, value: &str) {
44+
let _ = match mn_type {
45+
MnemonicsType::PROJECT => self.projects.insert(key.to_string(), value.to_string()),
46+
MnemonicsType::TAG => self.tags.insert(key.to_string(), value.to_string()),
47+
};
48+
}
49+
50+
pub fn remove(&mut self, mn_type: MnemonicsType, key: &str) {
51+
let _ = match mn_type {
52+
MnemonicsType::PROJECT => self.projects.remove(key),
53+
MnemonicsType::TAG => self.tags.remove(key),
54+
};
55+
}
56+
}
57+
58+
#[derive(Debug, Clone)]
59+
pub struct FileMnemonicsCache {
60+
cfg_path: Arc<Mutex<PathBuf>>,
61+
map: MnemonicsTable,
62+
}
63+
64+
impl FileMnemonicsCache {
65+
pub fn new(path: Arc<Mutex<PathBuf>>) -> Self {
66+
Self {
67+
cfg_path: path,
68+
map: MnemonicsTable::default(),
69+
}
70+
}
71+
72+
pub fn load(&mut self) -> Result<(), anyhow::Error> {
73+
let cfg_path_lck = self.cfg_path.lock().expect("Cannot lock file");
74+
let file = File::open(cfg_path_lck.as_path());
75+
if let Ok(mut file_obj) = file {
76+
let mut buf = String::new();
77+
let _ = file_obj.read_to_string(&mut buf);
78+
if !buf.is_empty() {
79+
let x: MnemonicsTable = toml::from_str(&buf).map_err(|p| {
80+
anyhow!("Could not parse configuration file: {}!", p.to_string())
81+
})?;
82+
self.map = x;
83+
}
84+
}
85+
Ok(())
86+
}
87+
}
88+
89+
impl MnemonicsCache for FileMnemonicsCache {
90+
fn insert(
91+
&mut self,
92+
mn_type: MnemonicsType,
93+
key: &str,
94+
value: &str,
95+
) -> Result<(), anyhow::Error> {
96+
// Ensure its unique. Check if the key is already used somewhere.
97+
let x = self
98+
.map
99+
.get(MnemonicsType::PROJECT)
100+
.values()
101+
.find(|p| p.as_str().eq(value));
102+
if x.is_some() {
103+
return Err(anyhow!("Duplicate key generated!"));
104+
}
105+
let x = self
106+
.map
107+
.get(MnemonicsType::TAG)
108+
.values()
109+
.find(|p| p.as_str().eq(value));
110+
if x.is_some() {
111+
return Err(anyhow!("Duplicate key generated!"));
112+
}
113+
114+
self.map.insert(mn_type, key, value);
115+
self.save()?;
116+
Ok(())
117+
}
118+
119+
fn remove(&mut self, mn_type: MnemonicsType, key: &str) -> Result<(), anyhow::Error> {
120+
self.map.remove(mn_type, &key);
121+
self.save()?;
122+
Ok(())
123+
}
124+
125+
fn get(&self, mn_type: MnemonicsType, key: &str) -> Option<String> {
126+
self.map.get(mn_type).get(key).cloned()
127+
}
128+
129+
fn save(&self) -> Result<(), anyhow::Error> {
130+
let p = self.cfg_path.lock().expect("Can lock file");
131+
let toml = toml::to_string(&self.map).unwrap();
132+
let mut f = File::create(p.as_path())?;
133+
let _ = f.write_all(toml.as_bytes());
134+
Ok(())
135+
}
136+
}
137+
138+
pub(crate) type MnemonicsCacheType = dyn MnemonicsCache + Send + Sync;
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use std::{io::{Read, Seek}, str::FromStr};
143+
144+
use super::*;
145+
use tempfile::NamedTempFile;
146+
147+
#[test]
148+
fn test_mnemonics_cache() {
149+
let mut file1 = NamedTempFile::new().expect("Cannot create named temp files.");
150+
let x = PathBuf::from(file1.path());
151+
let file_mtx = Arc::new(Mutex::new(x));
152+
153+
let mut mock = FileMnemonicsCache::new(file_mtx);
154+
assert_eq!(mock.get(MnemonicsType::PROJECT, "personal"), None);
155+
assert_eq!(
156+
mock.insert(MnemonicsType::TAG, "personal", "xz").is_ok(),
157+
true
158+
);
159+
assert_eq!(
160+
mock.get(MnemonicsType::TAG, "personal"),
161+
Some(String::from("xz"))
162+
);
163+
// how to validate content?
164+
file1.reopen().expect("Cannot reopen");
165+
let mut buf = String::new();
166+
let read_result = file1.read_to_string(&mut buf);
167+
assert_eq!(read_result.is_ok(), true);
168+
let read_result = read_result.expect("Could not read fro file");
169+
assert!(read_result > 0);
170+
assert_eq!(
171+
buf,
172+
String::from("[tags]\npersonal = \"xz\"\n\n[projects]\n")
173+
);
174+
assert_eq!(
175+
mock.insert(MnemonicsType::PROJECT, "taskwarrior", "xz")
176+
.is_ok(),
177+
false
178+
);
179+
assert_eq!(mock.remove(MnemonicsType::TAG, "personal").is_ok(), true);
180+
assert_eq!(mock.get(MnemonicsType::TAG, "personal"), None);
181+
assert_eq!(
182+
mock.insert(MnemonicsType::PROJECT, "taskwarrior", "xz")
183+
.is_ok(),
184+
true
185+
);
186+
assert_eq!(
187+
mock.insert(MnemonicsType::TAG, "personal", "xz").is_ok(),
188+
false
189+
);
190+
assert_eq!(mock.remove(MnemonicsType::PROJECT, "taskwarrior").is_ok(), true);
191+
file1.reopen().expect("Cannot reopen");
192+
let _ = file1.as_file().set_len(0);
193+
let _ = file1.seek(std::io::SeekFrom::Start(0));
194+
let data = String::from("[tags]\npersonal = \"xz\"\n\n[projects]\n");
195+
let _ = file1.write_all(data.as_bytes());
196+
let _ = file1.flush();
197+
assert_eq!(mock.load().is_ok(), true);
198+
assert_eq!(
199+
mock.get(MnemonicsType::TAG, "personal"),
200+
Some(String::from("xz"))
201+
);
202+
file1.reopen().expect("Cannot reopen");
203+
let _ = file1.as_file().set_len(0);
204+
let _ = file1.seek(std::io::SeekFrom::Start(0));
205+
let data = String::from("**********");
206+
let _ = file1.write_all(data.as_bytes());
207+
let _ = file1.flush();
208+
assert_eq!(mock.load().is_ok(), false);
209+
// Empty file cannot be parsed, but should not through an error!
210+
let _ = file1.as_file().set_len(0);
211+
let _ = file1.seek(std::io::SeekFrom::Start(0));
212+
let _ = file1.flush();
213+
assert_eq!(mock.load().is_ok(), true);
214+
// If the configuration file does not exist yet (close will delete),
215+
// it is fine as well.
216+
let _ = file1.close();
217+
assert_eq!(mock.load().is_ok(), true);
218+
219+
}
220+
221+
#[test]
222+
fn test_mnemonics_cache_file_fail() {
223+
let x = PathBuf::from_str("/4bda0a6b-da0d-46be-98e6-e06d43385fba/asdfa.cache").unwrap();
224+
let file_mtx = Arc::new(Mutex::new(x));
225+
226+
let mut mock = FileMnemonicsCache::new(file_mtx);
227+
assert_eq!(
228+
mock.insert(MnemonicsType::TAG, "personal", "xz").is_ok(),
229+
false
230+
);
231+
assert_eq!(mock.remove(MnemonicsType::PROJECT, "taskwarrior").is_ok(), false);
232+
}
233+
}

src/core/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
pub mod errors;
21
pub mod app;
3-
pub mod utils;
2+
pub mod cache;
3+
pub mod errors;
4+
pub mod utils;

src/core/utils.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use std::collections::HashSet;
22
use rand::distr::{Alphanumeric, SampleString};
3+
use tracing::debug;
4+
5+
use super::{app::AppState, cache::MnemonicsType};
36

47
pub fn make_shortcut(shortcuts: &mut HashSet<String>) -> String {
58
let alpha = Alphanumeric::default();
@@ -20,4 +23,27 @@ pub fn make_shortcut(shortcuts: &mut HashSet<String>) -> String {
2023
tries = 0;
2124
}
2225
}
26+
}
27+
28+
pub fn make_shortcut_cache(mn_type: MnemonicsType, key: &str, app_state: &AppState) -> String {
29+
let alpha = Alphanumeric::default();
30+
let mut len = 2;
31+
let mut tries = 0;
32+
loop {
33+
let shortcut = alpha.sample_string(&mut rand::rng(), len).to_lowercase();
34+
let shortcut_insert = app_state.app_cache.write().unwrap().insert(mn_type.clone(), key, &shortcut);
35+
if shortcut_insert.is_ok() {
36+
return shortcut;
37+
} else {
38+
debug!("Failed generating and saving shortcut {} - error: {:?}", shortcut, shortcut_insert.err());
39+
}
40+
tries += 1;
41+
if tries > 1000 {
42+
len += 1;
43+
if len > 3 {
44+
panic!("too many shortcuts! this should not happen");
45+
}
46+
tries = 0;
47+
}
48+
}
2349
}

0 commit comments

Comments
 (0)