Compare commits

...

3 commits

Author SHA1 Message Date
xeruf
a7c6cf2f59 style(main): consistent brace omittal 2025-01-29 23:06:42 +01:00
xeruf
807df3069a enhance: small user interface defaults 2025-01-29 23:04:54 +01:00
xeruf
e82f3479fa feat: multi-key filtering 2025-01-29 23:03:42 +01:00
5 changed files with 99 additions and 91 deletions

View file

@ -108,8 +108,8 @@ pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec<Tag>
if let Ok(key) = PublicKey::parse(&s[1..]) {
tags.push(Tag::public_key(key));
return false;
} else if let Some((key, _)) = users.find_user(&s[1..]) {
tags.push(Tag::public_key(*key));
} else if let Some((key, _)) = users.find_user(&s[1..]).first() {
tags.push(Tag::public_key(**key));
return false;
}
} else if s.starts_with('*') {

View file

@ -22,7 +22,7 @@ use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use std::collections::{HashMap, VecDeque};
use std::env;
use std::env::{args};
use std::env::args;
use std::fs;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
@ -170,11 +170,11 @@ async fn main() -> Result<()> {
read_keys(&keys_entry, &mut rl)?
};
info!("My active public key: {}", keys.public_key());
info!("Your active public key: {}", keys.public_key());
if args.peek().is_some_and(|arg| arg.trim_start_matches('-') == "export") {
let enc_pwd = read_password(&mut rl, "Please enter an encryption password for your secret key: ")?;
println!("Your encrypted key: {}", EncryptedSecretKey::new(keys.secret_key(), enc_pwd, 9, KeySecurity::Unknown)?.to_bech32()?);
if rl.readline("Do you want to erase your stored secret keys (y/n)? ")? == "y" {
if rl.readline("Do you want to erase your stored secret key (y/n)? ")? == "y" {
keys_entry.delete_credential()?;
}
// TODO optionally delete
@ -469,7 +469,7 @@ async fn main() -> Result<()> {
if tasks.custom_time.is_none() { tasks.move_up(); }
}
Some('&') => {
Some('&') =>
match arg {
None => tasks.undo(),
Some(text) => {
@ -506,9 +506,8 @@ async fn main() -> Result<()> {
}
}
}
}
Some('@') => {
Some('@') =>
match arg {
None => {
let today = Timestamp::now() - 80_000;
@ -517,12 +516,14 @@ async fn main() -> Result<()> {
continue 'repl;
}
}
Some("@") => {
tasks.reset_key_filter()
}
Some(arg) => {
if arg == "@" {
tasks.reset_key_filter()
} else if let Some((key, name)) = tasks.find_user(arg) {
info!("Showing {}'s tasks", name);
tasks.set_key_filter(key)
let users = tasks.find_users(arg);
if !users.is_empty() {
info!("Showing tasks for {}", users.iter().map(|(k, v)| v).join(", "));
tasks.set_key_filter(users.iter().map(|(k, v)| *k).collect_vec())
} else {
if parse_hour(arg, 1)
.or_else(|| parse_date(arg)
@ -536,10 +537,9 @@ async fn main() -> Result<()> {
}
}
}
};
}
}
Some('*') => {
Some('*') =>
match arg {
None => match tasks.get_position() {
None => {
@ -563,7 +563,6 @@ async fn main() -> Result<()> {
}
}
}
}
Some('|') =>
match arg {
@ -588,13 +587,12 @@ async fn main() -> Result<()> {
}
}
Some('?') => {
Some('?') =>
match arg {
None => tasks.set_state_filter(StateFilter::Default),
Some("?") => tasks.set_state_filter(StateFilter::All),
Some(arg) => tasks.set_state_filter(StateFilter::State(arg.to_string())),
}
}
Some('!') =>
match tasks.get_position() {
@ -629,11 +627,10 @@ async fn main() -> Result<()> {
}
}
Some('#') => {
Some('#') =>
if !tasks.update_tags(arg_default.split_whitespace().map(Hashtag::from)) {
continue;
}
}
Some('+') =>
match arg {
@ -665,7 +662,7 @@ async fn main() -> Result<()> {
}
}
println!("{}", tasks.times_tracked(max));
} else if let Some((key, _)) = tasks.find_user(arg) {
} else if let Some((key, _)) = tasks.find_users(arg).first() {
let (label, mut times) = tasks.times_tracked_for(&key);
println!("{}\n{}", label.italic(), times.join("\n"));
} else {
@ -679,7 +676,7 @@ async fn main() -> Result<()> {
continue 'repl;
}
Some(')') => {
Some(')') =>
match arg {
None => tasks.move_to(None),
Some(arg) => {
@ -691,7 +688,6 @@ async fn main() -> Result<()> {
continue 'repl;
}
}
}
Some('.') => {
let (remaining, dots) = trim_start_count(&command, '.');
@ -720,46 +716,47 @@ async fn main() -> Result<()> {
}
}
Some('/') => if arg.is_none() {
tasks.move_to(None);
} else {
let (remaining, dots) = trim_start_count(&command, '/');
let pos = tasks.up_by(dots - 1);
if remaining.is_empty() {
tasks.move_to(pos);
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
}
} else if let Ok(depth) = remaining.parse::<usize>() {
if pos != tasks.get_position() {
tasks.move_to(pos);
}
tasks.set_search_depth(depth);
Some('/') =>
if arg.is_none() {
tasks.move_to(None);
} else {
// TODO regex match
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
if !remaining.chars().any(|c| c.is_ascii_uppercase()) {
// Smart-case - case-sensitive if any uppercase char is entered
transform = Box::new(|s| s.to_ascii_lowercase());
}
let (remaining, dots) = trim_start_count(&command, '/');
let pos = tasks.up_by(dots - 1);
let filtered =
tasks.get_filtered(pos, |t| {
transform(&t.get_title()).contains(&remaining) ||
t.list_hashtags().any(
|tag| tag.contains(&remaining))
});
if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().next());
} else {
if remaining.is_empty() {
tasks.move_to(pos);
if !tasks.set_view(filtered) {
continue 'repl;
if dots > 1 {
info!("Moving up {} tasks", dots - 1)
}
} else if let Ok(depth) = remaining.parse::<usize>() {
if pos != tasks.get_position() {
tasks.move_to(pos);
}
tasks.set_search_depth(depth);
} else {
// TODO regex match
let mut transform: Box<dyn Fn(&str) -> String> = Box::new(|s: &str| s.to_string());
if !remaining.chars().any(|c| c.is_ascii_uppercase()) {
// Smart-case - case-sensitive if any uppercase char is entered
transform = Box::new(|s| s.to_ascii_lowercase());
}
let filtered =
tasks.get_filtered(pos, |t| {
transform(&t.get_title()).contains(&remaining) ||
t.list_hashtags().any(
|tag| tag.contains(&remaining))
});
if filtered.len() == 1 {
tasks.move_to(filtered.into_iter().next());
} else {
tasks.move_to(pos);
if !tasks.set_view(filtered) {
continue 'repl;
}
}
}
}
}
_ =>
if Regex::new("^wss?://").unwrap().is_match(command.trim()) {

View file

@ -93,7 +93,8 @@ pub(crate) struct TasksRelay {
state: StateFilter,
/// Current priority for filtering and new tasks
priority: Option<Prio>,
pubkey: Option<PublicKey>,
keys: Vec<PublicKey>,
own_keys: Vec<PublicKey>,
sender: EventSender,
overflow: VecDeque<Event>,
@ -192,7 +193,8 @@ impl TasksRelay {
tags_excluded: Default::default(),
state: Default::default(),
priority: None,
pubkey: Some(sender.pubkey()),
keys: vec![sender.pubkey()],
own_keys: vec![sender.pubkey()],
search_depth: 4,
view_depth: 0,
@ -234,6 +236,10 @@ impl TasksRelay {
#[inline]
pub(crate) fn len(&self) -> usize { self.tasks.len() }
fn own_keys(&self) -> &Vec<PublicKey> { &self.own_keys }
fn own_key(&self) -> PublicKey { self.sender.pubkey() }
pub(crate) fn get_position(&self) -> Option<EventId> {
self.get_position_at(now()).1
}
@ -278,7 +284,7 @@ impl TasksRelay {
/// Dynamic time tracking overview for current task or current user.
pub(crate) fn times_tracked(&self, limit: usize) -> String {
let (label, times) = self.times_tracked_with(&self.sender.pubkey());
let (label, times) = self.times_tracked_with(&self.own_key()); // TODO self.own_keys
let times = times.collect_vec();
format!("{}\n{}",
if times.is_empty() {
@ -352,8 +358,8 @@ impl TasksRelay {
let mut vec = Vec::with_capacity(set.len() / 2);
let mut iter = timestamps(set.values(), &ids).tuples();
while let Some(((start, _), (end, _))) = iter.next() {
// Filter out intervals <2 mins
if start.as_u64() + 120 < end.as_u64() {
// Filter out intervals <3 mins
if start.as_u64() + 200 < end.as_u64() {
vec.push(format!(
"{} - {} by {}",
format_timestamp_local(start),
@ -438,10 +444,10 @@ impl TasksRelay {
}
pub(crate) fn pubkey_str(&self) -> Option<String> {
match self.pubkey {
match self.keys.first() {
None => Some("ALL".to_string()),
Some(key) => {
if key != self.sender.pubkey() {
if &self.keys != self.own_keys() {
Some(self.users.get_username(&key))
} else {
None
@ -550,8 +556,9 @@ impl TasksRelay {
fn filter(&self, task: &Task) -> bool {
self.state.matches(task) &&
(!task.is_task() || self.pubkey.is_none_or(|p| p == task.get_owner() ||
task.list_hashtags().any(|t| t.matches(&self.users.get_username(&p))))) &&
(!task.is_task() || self.keys.is_empty() ||
self.keys.iter().any(|p| p == &task.get_owner() ||
task.list_hashtags().any(|t| t.matches(&self.users.get_username(&p))))) &&
self.priority.is_none_or(|prio| {
task.priority().unwrap_or(DEFAULT_PRIO) >= prio
}) &&
@ -722,8 +729,8 @@ impl TasksRelay {
}
}
pub(super) fn find_user(&self, name: &str) -> Option<(PublicKey, String)> {
self.users.find_user_with_displayname(name)
pub(super) fn find_users(&self, name: &str) -> Vec<(PublicKey, String)> {
self.users.find_user_with_displayname(name).collect()
}
// Movement and Selection
@ -749,19 +756,18 @@ impl TasksRelay {
}
pub(crate) fn reset_key_filter(&mut self) {
let own = self.sender.pubkey();
if self.pubkey.is_some_and(|k| k == own) {
if self.keys == self.own_keys {
self.view.clear();
info!("Showing everybody's tasks");
self.pubkey = None
self.keys.clear()
} else {
info!("Showing own tasks");
self.pubkey = Some(own)
self.keys = self.own_keys().clone();
}
}
pub(crate) fn set_key_filter(&mut self, key: PublicKey) {
self.pubkey = Some(key)
pub(crate) fn set_key_filter(&mut self, key: Vec<PublicKey>) {
self.keys = key
}
pub(crate) fn set_filter_since(&mut self, time: Timestamp) -> bool {
@ -805,7 +811,7 @@ impl TasksRelay {
pub(crate) fn clear_filters(&mut self) {
self.state = StateFilter::Default;
self.pubkey = Some(self.sender.pubkey());
self.keys = self.own_keys().clone();
self.priority = None;
self.view.clear();
self.tags.clear();
@ -1130,7 +1136,7 @@ impl TasksRelay {
if tags.iter().any(|t| t.kind() == TagKind::p()) {
None
} else {
self.pubkey.map(|p| Tag::public_key(p))
self.keys.first().map(|p| Tag::public_key(*p))
};
let prio =
if tags.iter().any(|t| t.kind().to_string() == PRIO) {

View file

@ -11,40 +11,43 @@ pub struct NostrUsers {
}
impl NostrUsers {
pub(crate) fn find_user_with_displayname(&self, term: &str) -> Option<(PublicKey, String)> {
pub(crate) fn find_user_with_displayname(&self, term: &str) -> impl Iterator<Item=(PublicKey, String)> + '_ {
self.find_user(term)
.into_iter()
.map(|(k, _)| (*k, self.get_displayname(k)))
}
// Find username or key starting with the given term.
pub(crate) fn find_user(&self, term: &str) -> Option<(&PublicKey, &Metadata)> {
pub(crate) fn find_user(&self, term: &str) -> Vec<(&PublicKey, &Metadata)> {
let lowered = term.trim().to_ascii_lowercase();
let term = lowered.as_str();
if term.is_empty() {
return None;
debug!("Tried to search user by empty term");
return vec![];
}
if let Ok(key) = PublicKey::from_str(term) {
return self.users.get_key_value(&key);
return self.users.get_key_value(&key).into_iter().collect();
}
self.users.iter()
.sorted_unstable_by_key(|(k, v)| self.get_user_time(k))
.rev()
.find(|(k, v)|
.filter(|(key, meta)|
// TODO regex word boundary
v.name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) ||
v.display_name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) ||
(term.len() > 4 && k.to_string().starts_with(term)))
meta.name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) ||
meta.display_name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) ||
(term.len() > 4 && key.to_string().starts_with(term)))
.collect()
}
pub(crate) fn get_displayname(&self, pubkey: &PublicKey) -> String {
self.users.get(pubkey)
.and_then(|m| m.display_name.clone().or(m.name.clone()))
.unwrap_or_else(|| pubkey.to_string())
.and_then(|meta| meta.display_name.clone().or(meta.name.clone()))
.map_or_else(|| pubkey.to_string(), |name| format!("{}@{:.6}", name, pubkey.to_string()))
}
pub(crate) fn get_username(&self, pubkey: &PublicKey) -> String {
self.users.get(pubkey)
.and_then(|m| m.name.clone())
.and_then(|meta| meta.name.clone())
.unwrap_or_else(|| format!("{:.6}", pubkey.to_string()))
}

View file

@ -156,7 +156,7 @@ fn test_context() {
//env_logger::init();
// ASSIGNEE
assert_eq!(tasks.pubkey, Some(tasks.sender.pubkey()));
assert_eq!(tasks.keys, vec![tasks.sender.pubkey()]);
let hoi = tasks.make_task("hoi").unwrap();
let hoi = tasks.get_by_id(&hoi).unwrap();
assert_eq!(hoi.get_owner(), tasks.sender.pubkey());
@ -169,12 +169,14 @@ fn test_context() {
let test1 = tasks.get_by_id(&test1id).unwrap();
assert_eq!(test1.get_owner(), pubkey);
tasks.pubkey = Some(pubkey);
tasks.set_key_filter(vec![pubkey]);
let test2id = tasks.make_task("test2").unwrap();
let test2 = tasks.get_by_id(&test2id).unwrap();
assert_eq!(test2.get_owner(), pubkey);
tasks.pubkey = None;
// First sets to own key then to all
tasks.reset_key_filter();
tasks.reset_key_filter();
let all = tasks.make_task("all").unwrap();
let all = tasks.get_by_id(&all).unwrap();
assert_eq!(all.get_assignee(), None);