Compare commits
No commits in common. "5dbea005620c5e417f396ea89ad9d3e53cec97b3" and "561fd9e1e58ac0e68c974b123c9d9002f34dfe39" have entirely different histories.
5dbea00562
...
561fd9e1e5
4 changed files with 24 additions and 56 deletions
14
README.md
14
README.md
|
@ -2,12 +2,6 @@
|
||||||
|
|
||||||
An immutable nested collaborative task manager, powered by nostr!
|
An immutable nested collaborative task manager, powered by nostr!
|
||||||
|
|
||||||
> Mostr is beta software.
|
|
||||||
> Do not entrust it exclusively with your data unless you know what you are doing!
|
|
||||||
|
|
||||||
> Intermediate versions might not properly persist all changes.
|
|
||||||
> A failed relay connection currently looses all intermediate changes.
|
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
First, start a nostr relay, such as
|
First, start a nostr relay, such as
|
||||||
|
@ -30,11 +24,6 @@ Install latest build:
|
||||||
|
|
||||||
cargo install --path .
|
cargo install --path .
|
||||||
|
|
||||||
This one-liner can help you stay on the latest version
|
|
||||||
(optionally add a `cd` to your mostr-directory in front):
|
|
||||||
|
|
||||||
git pull && cargo install --path . && mostr
|
|
||||||
|
|
||||||
Creating a test task externally:
|
Creating a test task externally:
|
||||||
`nostril --envelope --content "test task" --kind 1621 | websocat ws://localhost:4736`
|
`nostril --envelope --content "test task" --kind 1621 | websocat ws://localhost:4736`
|
||||||
|
|
||||||
|
@ -174,7 +163,6 @@ Append `@TIME` to any task creation or change command to record the action with
|
||||||
- with string argument, find first matching task in history
|
- with string argument, find first matching task in history
|
||||||
- with int argument, jump back X tasks in history
|
- with int argument, jump back X tasks in history
|
||||||
- undo last action (moving in place or upwards confirms pending actions)
|
- undo last action (moving in place or upwards confirms pending actions)
|
||||||
- `*` - (un)bookmark current task or list all bookmarks
|
|
||||||
- `wss://...` - switch or subscribe to relay (prefix with space to forcibly add a new one)
|
- `wss://...` - switch or subscribe to relay (prefix with space to forcibly add a new one)
|
||||||
|
|
||||||
Property Filters:
|
Property Filters:
|
||||||
|
@ -183,7 +171,7 @@ Property Filters:
|
||||||
- `+TAG` - add tag filter (empty: list all used tags)
|
- `+TAG` - add tag filter (empty: list all used tags)
|
||||||
- `-TAG` - remove tag filters (by prefix)
|
- `-TAG` - remove tag filters (by prefix)
|
||||||
- `?STATUS` - set status filter (type or description) - plain `?` to reset, `??` to show all
|
- `?STATUS` - set status filter (type or description) - plain `?` to reset, `??` to show all
|
||||||
- `*INT` - set priority filter - `**` to reset
|
- `*INT` - set priority filter
|
||||||
- `@[AUTHOR|TIME]` - filter by time or author (pubkey, or `@` for self, TBI: id prefix, name prefix)
|
- `@[AUTHOR|TIME]` - filter by time or author (pubkey, or `@` for self, TBI: id prefix, name prefix)
|
||||||
|
|
||||||
Status descriptions can be used for example for Kanban columns or review flows.
|
Status descriptions can be used for example for Kanban columns or review flows.
|
||||||
|
|
|
@ -5,6 +5,7 @@ use log::info;
|
||||||
use nostr_sdk::TagStandard::Hashtag;
|
use nostr_sdk::TagStandard::Hashtag;
|
||||||
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard};
|
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagKind, TagStandard};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::iter::once;
|
||||||
|
|
||||||
pub const TASK_KIND: Kind = Kind::GitIssue;
|
pub const TASK_KIND: Kind = Kind::GitIssue;
|
||||||
pub const PROCEDURE_KIND_ID: u16 = 1639;
|
pub const PROCEDURE_KIND_ID: u16 = 1639;
|
||||||
|
@ -149,10 +150,8 @@ pub(crate) fn to_prio_tag(value: Prio) -> Tag {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extract_tags() {
|
fn test_extract_tags() {
|
||||||
assert_eq!(extract_tags("Hello from #mars with #greetings *4 # # yeah done-it"),
|
assert_eq!(extract_tags("Hello from #mars with #greetings *4 # yeah done-it"),
|
||||||
("Hello from #mars with #greetings".to_string(),
|
("Hello from #mars with #greetings".to_string(),
|
||||||
["mars", "greetings", "yeah", "done-it"].into_iter().map(to_hashtag)
|
["mars", "greetings", "yeah", "done-it"].into_iter().map(to_hashtag)
|
||||||
.chain(std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))).collect()));
|
.chain(once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))).collect()))
|
||||||
assert_eq!(extract_tags("So tagless #"),
|
|
||||||
("So tagless".to_string(), vec![]));
|
|
||||||
}
|
}
|
38
src/main.rs
38
src/main.rs
|
@ -4,7 +4,8 @@ use std::env::{args, var};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
use std::ops::Sub;
|
use std::iter::once;
|
||||||
|
use std::ops::{Add, Sub};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -27,7 +28,7 @@ use tokio::time::timeout;
|
||||||
|
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND};
|
use crate::kinds::{Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS, TRACKING_KIND};
|
||||||
use crate::task::{State, Task, TaskState};
|
use crate::task::{State, Task, TaskState, MARKER_DEPENDS};
|
||||||
use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
|
use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
|
||||||
|
|
||||||
mod helpers;
|
mod helpers;
|
||||||
|
@ -447,18 +448,20 @@ async fn main() -> Result<()> {
|
||||||
Some(',') =>
|
Some(',') =>
|
||||||
match arg {
|
match arg {
|
||||||
None => {
|
None => {
|
||||||
if let Some(task) = tasks.get_current_task() {
|
match tasks.get_current_task() {
|
||||||
let mut desc = task.description_events().peekable();
|
None => {
|
||||||
if desc.peek().is_some() {
|
info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes");
|
||||||
|
tasks.recurse_activities = !tasks.recurse_activities;
|
||||||
|
info!("Toggled activities recursion to {}", tasks.recurse_activities);
|
||||||
|
}
|
||||||
|
Some(task) => {
|
||||||
println!("{}",
|
println!("{}",
|
||||||
desc.map(|e| format!("{} {}", format_timestamp_local(&e.created_at), e.content))
|
task.description_events()
|
||||||
|
.map(|e| format!("{} {}", format_timestamp_local(&e.created_at), e.content))
|
||||||
.join("\n"));
|
.join("\n"));
|
||||||
continue 'repl;
|
continue 'repl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!("With a task selected, use ,NOTE to attach NOTE and , to list all its notes");
|
|
||||||
tasks.recurse_activities = !tasks.recurse_activities;
|
|
||||||
info!("Toggled activities recursion to {}", tasks.recurse_activities);
|
|
||||||
}
|
}
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if arg.len() < CHARACTER_THRESHOLD {
|
if arg.len() < CHARACTER_THRESHOLD {
|
||||||
|
@ -534,8 +537,7 @@ async fn main() -> Result<()> {
|
||||||
match arg {
|
match arg {
|
||||||
None => match tasks.get_position() {
|
None => match tasks.get_position() {
|
||||||
None => {
|
None => {
|
||||||
info!("Showing only bookmarked tasks");
|
tasks.set_priority(None);
|
||||||
tasks.set_view_bookmarks();
|
|
||||||
}
|
}
|
||||||
Some(pos) =>
|
Some(pos) =>
|
||||||
match or_warn!(tasks.toggle_bookmark(pos)) {
|
match or_warn!(tasks.toggle_bookmark(pos)) {
|
||||||
|
@ -546,7 +548,8 @@ async fn main() -> Result<()> {
|
||||||
},
|
},
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if arg == "*" {
|
if arg == "*" {
|
||||||
tasks.set_priority(None);
|
info!("Showing only bookmarked tasks");
|
||||||
|
tasks.set_view_bookmarks();
|
||||||
} else {
|
} else {
|
||||||
tasks.set_priority(arg.parse()
|
tasks.set_priority(arg.parse()
|
||||||
.inspect_err(|e| warn!("Invalid Priority {arg}: {e}")).ok()
|
.inspect_err(|e| warn!("Invalid Priority {arg}: {e}")).ok()
|
||||||
|
@ -620,21 +623,18 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some('#') => {
|
Some('#') =>
|
||||||
if !tasks.update_tags(arg_default.split_whitespace().map(|s| Hashtag(s.to_string()).into())) {
|
tasks.set_tags(arg_default.split_whitespace().map(|s| Hashtag(s.to_string()).into())),
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some('+') =>
|
Some('+') =>
|
||||||
match arg {
|
match arg {
|
||||||
Some(arg) => tasks.add_tag(arg.to_string()),
|
Some(arg) => tasks.add_tag(arg.to_string()),
|
||||||
None => {
|
None => {
|
||||||
tasks.print_hashtags();
|
println!("Hashtags of all known tasks:\n{}", tasks.all_hashtags().join(" ").italic());
|
||||||
if tasks.has_tag_filter() {
|
if tasks.has_tag_filter() {
|
||||||
println!("Use # to remove tag filters and . to remove all filters.")
|
println!("Use # to remove tag filters and . to remove all filters.")
|
||||||
}
|
}
|
||||||
continue;
|
continue 'repl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
src/tasks.rs
21
src/tasks.rs
|
@ -647,26 +647,7 @@ impl TasksRelay {
|
||||||
!self.tags.is_empty() || !self.tags_excluded.is_empty()
|
!self.tags.is_empty() || !self.tags_excluded.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_hashtags(&self) {
|
pub(crate) fn set_tags(&mut self, tags: impl IntoIterator<Item=Tag>) {
|
||||||
println!("Hashtags of all known tasks:\n{}", self.all_hashtags().join(" ").italic());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if tags have been updated, false if it printed something
|
|
||||||
pub(crate) fn update_tags(&mut self, tags: impl IntoIterator<Item=Tag>) -> bool {
|
|
||||||
let mut peekable = tags.into_iter().peekable();
|
|
||||||
if self.tags.is_empty() && peekable.peek().is_none() {
|
|
||||||
if !self.tags_excluded.is_empty() {
|
|
||||||
self.tags_excluded.clear();
|
|
||||||
}
|
|
||||||
self.print_hashtags();
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
self.set_tags(peekable);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_tags(&mut self, tags: impl IntoIterator<Item=Tag>) {
|
|
||||||
self.tags.clear();
|
self.tags.clear();
|
||||||
self.tags.extend(tags);
|
self.tags.extend(tags);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue