Compare commits
7 Commits
9ad1243078
...
8bf305d4d4
Author | SHA1 | Date |
---|---|---|
xeruf | 8bf305d4d4 | |
xeruf | eaeeebca7b | |
xeruf | 2255abc1b8 | |
xeruf | 19d0fbb8fc | |
xeruf | 903536bd3b | |
xeruf | 86654c8348 | |
xeruf | d88cae4273 |
42
README.md
42
README.md
|
@ -105,8 +105,9 @@ To stop time-tracking completely, simply move to the root of all tasks.
|
|||
|
||||
Dot or slash can be repeated to move to parent tasks before acting.
|
||||
|
||||
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (1-indexed)
|
||||
- `::[PROP]` - Sort by property PROP (multiple space-separated values allowed)
|
||||
- `:[IND][PROP]` - add property column PROP at IND or end, if it already exists remove property column PROP or IND (
|
||||
1-indexed), empty: list properties
|
||||
- `::[PROP]` - sort by property PROP (multiple space-separated values allowed)
|
||||
- `([TIME]` - list tracked times or insert timetracking with the specified offset
|
||||
such as `-1d`, `-15 minutes`, `yesterday 17:20`, `in 2 fortnights`
|
||||
- `)[TIME]` - stop timetracking with optional offset - also convenience helper to move to root
|
||||
|
@ -114,8 +115,8 @@ Dot or slash can be repeated to move to parent tasks before acting.
|
|||
- `<[TEXT]` - close active task and move up, with optional status description
|
||||
- `!TEXT` - set status for current task from text and move up (empty: Open)
|
||||
- `,[TEXT]` - list notes or add text note (comment / description)
|
||||
- TBI: `*[INT]` - set priority - can also be used in task creation, with any digit
|
||||
- TBI: status history and creation with attribution
|
||||
- `*[INT]` - set priority - can also be used in task creation, with any digit
|
||||
- TBI: show status history and creation with attribution
|
||||
- `&` - undo last action (moving in place or upwards confirms pending actions)
|
||||
- `wss://...` - switch or subscribe to relay (prefix with space to forcibly add a new one)
|
||||
|
||||
|
@ -126,32 +127,12 @@ Property Filters:
|
|||
- `-TAG` - remove tag filters by prefix
|
||||
- `?STATUS` - filter by status (type or description) - plain `?` to reset, `??` to show all
|
||||
- `@AUTHOR` - filter by author (`@` for self, id prefix, name prefix)
|
||||
- TBI: `**INT` - filter by priority
|
||||
- TBI: Filter by time
|
||||
|
||||
Status descriptions can be used for example for Kanban columns or review flows.
|
||||
An active tag or status filter will also set that attribute for newly created tasks.
|
||||
|
||||
### Available Columns
|
||||
|
||||
- `id`
|
||||
- `parentid`
|
||||
- `name`
|
||||
- `state` - indicator of current progress
|
||||
- `status` - pure task status
|
||||
- `hashtags` - list of hashtags set for the task
|
||||
- `tags` - values of all nostr tags associated with the event, except event tags
|
||||
- `desc` - last note on the task
|
||||
- `description` - accumulated notes on the task
|
||||
- `path` - name including parent tasks
|
||||
- `rpath` - name including parent tasks up to active task
|
||||
- `time` - time tracked on this task
|
||||
- `rtime` - time tracked on this tasks and all recursive subtasks
|
||||
- `progress` - recursive subtask completion in percent
|
||||
- `subtasks` - how many direct subtasks are complete
|
||||
- TBI `depends`
|
||||
|
||||
For debugging: `props`, `alltags`, `descriptions`
|
||||
|
||||
### Notes
|
||||
|
||||
- TBI = To Be Implemented
|
||||
|
@ -160,12 +141,13 @@ For debugging: `props`, `alltags`, `descriptions`
|
|||
## Nostr reference
|
||||
|
||||
Mostr mainly uses the following NIPs:
|
||||
- Kind 1 for task descriptions
|
||||
|
||||
- Kind 1 for task descriptions and permanent tasks, can contain task property updates (tags, priority)
|
||||
- Issue Tracking: https://github.com/nostr-protocol/nips/blob/master/34.md
|
||||
+ Tasks have Kind 1621 (originally: git issue - currently no native markdown support)
|
||||
+ Kind 1622 may be used for task comments or replace Kind 1 for descriptions
|
||||
+ Tasks have Kind 1621 (originally: git issue - currently no markdown support implemented)
|
||||
+ TBI: Kind 1622 for task comments
|
||||
+ Kind 1630-1633: Task Status (1630 Open, 1631 Done, 1632 Closed, 1633 Pending)
|
||||
- Implementing proprietary Kind 1650 for time-tracking
|
||||
- Own Kind 1650 for time-tracking
|
||||
|
||||
Considering to use Calendar: https://github.com/nostr-protocol/nips/blob/master/52.md
|
||||
- Kind 31922 for GANTT, since it has only Date
|
||||
|
@ -207,6 +189,8 @@ The following features are not ready to be implemented
|
|||
because they need conceptualization.
|
||||
Suggestions welcome!
|
||||
|
||||
- Do not track time on Closed task?
|
||||
- Allow adding new parent via description?
|
||||
- Special commands: help, exit, tutorial, change log level
|
||||
- Duplicate task (subtasks? timetracking?)
|
||||
- What if I want to postpone a procedure, i.e. make it pending, or move it across kanban, does this make sense?
|
||||
|
|
|
@ -32,7 +32,8 @@ pub fn prompt(prompt: &str) -> Option<String> {
|
|||
|
||||
// For use in format strings but not possible, so need global find-replace
|
||||
pub const MAX_TIMESTAMP_WIDTH: u8 = 15;
|
||||
/// Format nostr Timestamp relative to local time with optional day specifier or full date if needed
|
||||
/// Format nostr Timestamp relative to local time
|
||||
/// with optional day specifier or full date depending on distance to today
|
||||
pub fn relative_datetimestamp(stamp: &Timestamp) -> String {
|
||||
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
|
||||
Single(time) => {
|
||||
|
@ -54,11 +55,12 @@ pub fn relative_datetimestamp(stamp: &Timestamp) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Format a nostr timestamp in a sensible comprehensive format
|
||||
pub fn local_datetimestamp(stamp: &Timestamp) -> String {
|
||||
format_stamp(stamp, "%y-%m-%d %a %H:%M")
|
||||
}
|
||||
|
||||
|
||||
/// Format a nostr timestamp with the given format
|
||||
pub fn format_stamp(stamp: &Timestamp, format: &str) -> String {
|
||||
match Local.timestamp_opt(stamp.as_u64() as i64, 0) {
|
||||
Single(time) => time.format(format).to_string(),
|
||||
|
|
76
src/kinds.rs
76
src/kinds.rs
|
@ -1,27 +1,47 @@
|
|||
use itertools::Itertools;
|
||||
use log::info;
|
||||
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
|
||||
use nostr_sdk::TagStandard::Hashtag;
|
||||
|
||||
pub const METADATA_KIND: u16 = 0;
|
||||
pub const NOTE_KIND: u16 = 1;
|
||||
pub const TASK_KIND: u16 = 1621;
|
||||
pub const PROCEDURE_KIND: u16 = 1639;
|
||||
pub const TRACKING_KIND: u16 = 1650;
|
||||
pub const KINDS: [u16; 8] = [1, TASK_KIND, TRACKING_KIND, PROCEDURE_KIND, 1630, 1631, 1632, 1633];
|
||||
pub const KINDS: [u16; 9] = [
|
||||
METADATA_KIND,
|
||||
NOTE_KIND,
|
||||
TASK_KIND,
|
||||
TRACKING_KIND,
|
||||
PROCEDURE_KIND,
|
||||
1630, 1631, 1632, 1633];
|
||||
|
||||
pub const PROPERTY_COLUMNS: &str = "Available properties:
|
||||
- `id`
|
||||
- `parentid`
|
||||
- `name`
|
||||
- `state`
|
||||
- `hashtags`
|
||||
/// Helper for available properties.
|
||||
/// TODO: use formatting - bold / heading / italics - and generate from code
|
||||
pub const PROPERTY_COLUMNS: &str =
|
||||
"# Available Properties
|
||||
Immutable:
|
||||
- `id` - unique task id
|
||||
- `parentid` - unique task id of the parent, if any
|
||||
- `name` - initial name of the task
|
||||
- `created` - task creation timestamp
|
||||
- `author` - name of the task creator
|
||||
Task:
|
||||
- `status` - pure task status
|
||||
- `hashtags` - list of hashtags set for the task
|
||||
- `tags` - values of all nostr tags associated with the event, except event tags
|
||||
- `desc` - last note on the task
|
||||
- `description` - accumulated notes on the task
|
||||
- `path` - name including parent tasks
|
||||
- `rpath` - name including parent tasks up to active task
|
||||
- `time` - time tracked on this task by you
|
||||
Utilities:
|
||||
- `state` - indicator of current progress
|
||||
- `rtime` - time tracked on this tasks and its subtree by everyone
|
||||
- `progress` - recursive subtask completion in percent
|
||||
- `subtasks` - how many direct subtasks are complete";
|
||||
- `subtasks` - how many direct subtasks are complete
|
||||
- `path` - name including parent tasks
|
||||
- `rpath` - name including parent tasks up to active task
|
||||
- TBI `depends` - list all tasks this task depends on before it becomes actionable
|
||||
Debugging: `pubkey`, `props`, `alltags`, `descriptions`";
|
||||
|
||||
pub(crate) fn build_tracking<I>(id: I) -> EventBuilder
|
||||
where
|
||||
|
@ -34,9 +54,39 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_task(name: &str, tags: Vec<Tag>) -> EventBuilder {
|
||||
info!("Created task \"{name}\" with tags [{}]", tags.iter().map(|tag| format_tag(tag)).join(", "));
|
||||
EventBuilder::new(Kind::from(TASK_KIND), name, tags)
|
||||
/// Build a task with informational output and optional labeled kind
|
||||
pub(crate) fn build_task(name: &str, tags: Vec<Tag>, kind: Option<(&str, Kind)>) -> EventBuilder {
|
||||
info!("Created {}task \"{name}\" with tags [{}]",
|
||||
kind.map(|k| k.0).unwrap_or_default(),
|
||||
tags.iter().map(|tag| format_tag(tag)).join(", "));
|
||||
EventBuilder::new(kind.map(|k| k.1).unwrap_or(Kind::from(TASK_KIND)), name, tags)
|
||||
}
|
||||
|
||||
pub(crate) fn build_prop(
|
||||
kind: Kind,
|
||||
comment: &str,
|
||||
id: EventId,
|
||||
) -> EventBuilder {
|
||||
EventBuilder::new(
|
||||
kind,
|
||||
comment,
|
||||
vec![Tag::event(id)],
|
||||
)
|
||||
}
|
||||
|
||||
/// Expects sanitized input
|
||||
pub(crate) fn extract_tags(input: &str) -> (&str, Vec<Tag>) {
|
||||
match input.split_once(": ") {
|
||||
None => (input, vec![]),
|
||||
Some(s) => {
|
||||
let tags = s
|
||||
.1
|
||||
.split_ascii_whitespace()
|
||||
.map(|t| Hashtag(t.to_string()).into())
|
||||
.collect();
|
||||
(s.0, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_tag(tag: &Tag) -> String {
|
||||
|
|
58
src/main.rs
58
src/main.rs
|
@ -180,21 +180,11 @@ async fn main() {
|
|||
},
|
||||
}
|
||||
|
||||
let sub_id = client.subscribe(vec![Filter::new().kinds(KINDS.into_iter().map(|k| Kind::from(k)))], None).await;
|
||||
let sub_id = client.subscribe(vec![
|
||||
Filter::new().kinds(KINDS.into_iter().map(|k| Kind::from(k)))
|
||||
], None).await;
|
||||
info!("Subscribed with {:?}", sub_id);
|
||||
|
||||
// TODO user data from config file or home relay?
|
||||
//let metadata = Metadata::new()
|
||||
// .name("username")
|
||||
// .display_name("My Username")
|
||||
// .about("Description")
|
||||
// .picture(Url::parse("https://example.com/avatar.png")?)
|
||||
// .banner(Url::parse("https://example.com/banner.png")?)
|
||||
// .nip05("username@example.com")
|
||||
// .lud16("yuki@getalby.com")
|
||||
// .custom_field("custom_field", "my value");
|
||||
//client.set_metadata(&metadata).await?;
|
||||
|
||||
let mut notifications = client.notifications();
|
||||
client.connect().await;
|
||||
|
||||
|
@ -207,6 +197,19 @@ async fn main() {
|
|||
let mut queue: Option<(Url, Vec<Event>)> = None;
|
||||
|
||||
loop {
|
||||
if let Ok(user) = var("USER") {
|
||||
let metadata = Metadata::new()
|
||||
.name(user);
|
||||
// .display_name("My Username")
|
||||
// .about("Description")
|
||||
// .picture(Url::parse("https://example.com/avatar.png")?)
|
||||
// .banner(Url::parse("https://example.com/banner.png")?)
|
||||
// .nip05("username@example.com")
|
||||
// .lud16("yuki@getalby.com")
|
||||
// .custom_field("custom_field", "my value");
|
||||
or_print(client.set_metadata(&metadata).await);
|
||||
}
|
||||
|
||||
let result_received = rx.recv_timeout(Duration::from_secs(INACTVITY_DELAY));
|
||||
match result_received {
|
||||
Ok(MostrMessage::NewRelay(url)) => {
|
||||
|
@ -366,21 +369,31 @@ async fn main() {
|
|||
tasks.move_up();
|
||||
}
|
||||
|
||||
Some('@') | Some('&') => {
|
||||
Some('&') => {
|
||||
tasks.undo();
|
||||
}
|
||||
|
||||
Some('@') => {
|
||||
let author = arg.and_then(|a| PublicKey::from_str(a).ok()).unwrap_or_else(|| keys.public_key());
|
||||
info!("Filtering for events by {author}");
|
||||
tasks.set_filter(
|
||||
tasks.filtered_tasks(tasks.get_position())
|
||||
.filter(|t| t.event.pubkey == author)
|
||||
.map(|t| t.event.id)
|
||||
.collect()
|
||||
)
|
||||
}
|
||||
|
||||
Some('*') => {
|
||||
info!("Setting priority not yet implemented")
|
||||
}
|
||||
|
||||
Some('|') =>
|
||||
match arg {
|
||||
None => match tasks.get_position() {
|
||||
None => {
|
||||
info!("Filtering for Procedures");
|
||||
tasks.set_filter(
|
||||
tasks.filtered_tasks(None)
|
||||
.filter(|t| t.pure_state() == State::Procedure)
|
||||
.map(|t| t.event.id)
|
||||
.collect()
|
||||
);
|
||||
tasks.set_state_filter(
|
||||
StateFilter::State(State::Procedure.to_string()));
|
||||
}
|
||||
Some(id) => {
|
||||
tasks.set_state_for(id, "", State::Procedure);
|
||||
|
@ -392,8 +405,7 @@ async fn main() {
|
|||
tasks.move_up();
|
||||
tasks.make_task_with(
|
||||
arg,
|
||||
once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS))
|
||||
.chain(tasks.parent_tag()),
|
||||
once(tasks.make_event_tag_from_id(pos, MARKER_DEPENDS)),
|
||||
true);
|
||||
break 'arm;
|
||||
}
|
||||
|
|
31
src/task.rs
31
src/task.rs
|
@ -10,8 +10,8 @@ use itertools::Itertools;
|
|||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp};
|
||||
|
||||
use crate::helpers::some_non_empty;
|
||||
use crate::kinds::{is_hashtag, PROCEDURE_KIND};
|
||||
use crate::helpers::{local_datetimestamp, some_non_empty};
|
||||
use crate::kinds::{is_hashtag, PROCEDURE_KIND, TASK_KIND};
|
||||
|
||||
pub static MARKER_PARENT: &str = "parent";
|
||||
pub static MARKER_DEPENDS: &str = "depends";
|
||||
|
@ -95,6 +95,11 @@ impl Task {
|
|||
self.description_events().map(|e| &e.content)
|
||||
}
|
||||
|
||||
pub(crate) fn is_task(&self) -> bool {
|
||||
self.event.kind.as_u16() == TASK_KIND ||
|
||||
self.states().next().is_some()
|
||||
}
|
||||
|
||||
fn states(&self) -> impl Iterator<Item=TaskState> + '_ {
|
||||
self.props.iter().filter_map(|event| {
|
||||
event.kind.try_into().ok().map(|s| TaskState {
|
||||
|
@ -117,6 +122,13 @@ impl Task {
|
|||
self.state().unwrap_or_else(|| self.default_state())
|
||||
}
|
||||
|
||||
/// Returns None for a stateless task.
|
||||
pub(crate) fn state_label(&self) -> Option<ColoredString> {
|
||||
self.state()
|
||||
.or_else(|| Some(self.default_state()).filter(|_| self.is_task()))
|
||||
.map(|state| state.get_colored_label())
|
||||
}
|
||||
|
||||
fn default_state(&self) -> TaskState {
|
||||
TaskState {
|
||||
name: None,
|
||||
|
@ -139,10 +151,14 @@ impl Task {
|
|||
|
||||
pub(crate) fn get(&self, property: &str) -> Option<String> {
|
||||
match property {
|
||||
// Static
|
||||
"id" => Some(self.event.id.to_string()),
|
||||
"parentid" => self.parent_id().map(|i| i.to_string()),
|
||||
"status" => Some(self.state_or_default().get_label()),
|
||||
"name" => Some(self.event.content.clone()),
|
||||
"pubkey" => Some(self.event.pubkey.to_string()),
|
||||
"created" => Some(local_datetimestamp(&self.event.created_at)),
|
||||
// Dynamic
|
||||
"status" => self.state_label().map(|c| c.to_string()),
|
||||
"desc" => self.descriptions().last().cloned(),
|
||||
"description" => Some(self.descriptions().join(" ")),
|
||||
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }),
|
||||
|
@ -205,11 +221,16 @@ impl Display for TaskState {
|
|||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Ord, PartialOrd, Eq)]
|
||||
pub(crate) enum State {
|
||||
Open,
|
||||
/// Actionable
|
||||
Open = 1630,
|
||||
/// Completed
|
||||
Done,
|
||||
/// Not Actionable (anymore)
|
||||
Closed,
|
||||
/// Temporarily not actionable
|
||||
Pending,
|
||||
Procedure,
|
||||
/// Actionable ordered task list
|
||||
Procedure = PROCEDURE_KIND as isize,
|
||||
}
|
||||
impl From<&str> for State {
|
||||
fn from(value: &str) -> Self {
|
||||
|
|
129
src/tasks.rs
129
src/tasks.rs
|
@ -1,7 +1,7 @@
|
|||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::{Error, stdout, Write};
|
||||
use std::iter::once;
|
||||
use std::iter::{empty, once};
|
||||
use std::ops::{Div, Rem};
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc::Sender;
|
||||
|
@ -11,7 +11,7 @@ use chrono::Local;
|
|||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::{Event, EventBuilder, EventId, Keys, Kind, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
|
||||
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
|
||||
use nostr_sdk::prelude::Marker;
|
||||
use TagStandard::Hashtag;
|
||||
|
||||
|
@ -27,26 +27,29 @@ pub(crate) struct Tasks {
|
|||
tasks: TaskMap,
|
||||
/// History of active tasks by PubKey
|
||||
history: HashMap<PublicKey, BTreeSet<Event>>,
|
||||
/// Index of found users with metadata
|
||||
users: HashMap<PublicKey, Metadata>,
|
||||
|
||||
/// The task properties currently visible
|
||||
properties: Vec<String>,
|
||||
/// The task properties sorted by
|
||||
sorting: VecDeque<String>,
|
||||
|
||||
/// Currently active task
|
||||
position: Option<EventId>,
|
||||
/// A filtered view of the current tasks
|
||||
view: Vec<EventId>,
|
||||
/// Negative: Only Leaf nodes
|
||||
/// Zero: Only Active node
|
||||
/// Positive: Go down the respective level
|
||||
depth: i8,
|
||||
|
||||
/// Currently active task
|
||||
position: Option<EventId>,
|
||||
/// Currently active tags
|
||||
tags: BTreeSet<Tag>,
|
||||
/// Tags filtered out
|
||||
tags_excluded: BTreeSet<Tag>,
|
||||
/// Current active state
|
||||
state: StateFilter,
|
||||
/// A filtered view of the current tasks
|
||||
view: Vec<EventId>,
|
||||
|
||||
sender: EventSender,
|
||||
}
|
||||
|
@ -118,6 +121,7 @@ impl Tasks {
|
|||
Tasks {
|
||||
tasks: Default::default(),
|
||||
history: Default::default(),
|
||||
users: Default::default(),
|
||||
properties: vec![
|
||||
"state".into(),
|
||||
"rtime".into(),
|
||||
|
@ -329,12 +333,21 @@ impl Tasks {
|
|||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn referenced_tasks<F: Fn(&mut Task)>(&mut self, event: &Event, f: F) {
|
||||
/// Executes the given function with each task referenced by this event without marker.
|
||||
/// Returns true if any task was found.
|
||||
pub(crate) fn referenced_tasks<F: Fn(&mut Task)>(&mut self, event: &Event, f: F) -> bool {
|
||||
let mut found = false;
|
||||
for tag in event.tags.iter() {
|
||||
if let Some(TagStandard::Event { event_id, .. }) = tag.as_standardized() {
|
||||
self.tasks.get_mut(event_id).map(|t| f(t));
|
||||
if let Some(TagStandard::Event { event_id, marker, .. }) = tag.as_standardized() {
|
||||
if marker.is_none() {
|
||||
self.tasks.get_mut(event_id).map(|t| {
|
||||
found = true;
|
||||
f(t)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -455,14 +468,16 @@ impl Tasks {
|
|||
if let Some(task) = task.get_dependendees().iter().filter_map(|id| self.get_by_id(id)).find(|t| t.pure_state().is_open()) {
|
||||
return format!("Blocked by \"{}\"", task.get_title()).bright_red().to_string();
|
||||
}
|
||||
let state = task.state_or_default();
|
||||
if state.state.is_open() && progress.is_some_and(|p| p > 0.1) {
|
||||
state.state.colorize(&prog_string)
|
||||
let state = task.pure_state();
|
||||
if state.is_open() && progress.is_some_and(|p| p > 0.1) {
|
||||
state.colorize(&prog_string)
|
||||
} else {
|
||||
state.get_colored_label()
|
||||
task.state_label().unwrap_or_default()
|
||||
}.to_string()
|
||||
}
|
||||
"progress" => prog_string.clone(),
|
||||
|
||||
"author" => self.get_author(&task.event.pubkey),
|
||||
"path" => self.get_task_path(Some(task.event.id)),
|
||||
"rpath" => self.relative_path(task.event.id),
|
||||
// TODO format strings configurable
|
||||
|
@ -472,6 +487,12 @@ impl Tasks {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_author(&self, pubkey: &PublicKey) -> String {
|
||||
self.users.get(pubkey)
|
||||
.and_then(|m| m.name.clone())
|
||||
.unwrap_or_else(|| pubkey.to_string())
|
||||
}
|
||||
|
||||
// Movement and Selection
|
||||
|
||||
pub(crate) fn set_filter(&mut self, view: Vec<EventId>) {
|
||||
|
@ -528,7 +549,7 @@ impl Tasks {
|
|||
self.sender.flush();
|
||||
}
|
||||
|
||||
/// Returns ids of tasks matching the filter.
|
||||
/// Returns ids of tasks starting with the given string.
|
||||
pub(crate) fn get_filtered(&self, arg: &str) -> Vec<EventId> {
|
||||
if let Ok(id) = EventId::parse(arg) {
|
||||
return vec![id];
|
||||
|
@ -597,24 +618,6 @@ impl Tasks {
|
|||
|
||||
// Updates
|
||||
|
||||
/// Expects sanitized input
|
||||
pub(crate) fn parse_task(&self, input: &str) -> EventBuilder {
|
||||
let mut tags: Vec<Tag> = self.tags.iter().cloned().collect();
|
||||
match input.split_once(": ") {
|
||||
None => build_task(input, tags),
|
||||
Some(s) => {
|
||||
tags.append(
|
||||
&mut s
|
||||
.1
|
||||
.split_ascii_whitespace()
|
||||
.map(|t| Hashtag(t.to_string()).into())
|
||||
.collect(),
|
||||
);
|
||||
build_task(s.0, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn make_event_tag_from_id(&self, id: EventId, marker: &str) -> Tag {
|
||||
Tag::from(TagStandard::Event {
|
||||
event_id: id,
|
||||
|
@ -655,20 +658,23 @@ impl Tasks {
|
|||
/// Creates a task following the current state
|
||||
/// Sanitizes input
|
||||
pub(crate) fn make_task(&mut self, input: &str) -> EventId {
|
||||
self.make_task_with(input, self.position_tags(), true)
|
||||
self.make_task_with(input, empty(), true)
|
||||
}
|
||||
|
||||
pub(crate) fn make_task_and_enter(&mut self, input: &str, state: State) {
|
||||
let id = self.make_task_with(input, self.position_tags(), false);
|
||||
let id = self.make_task_with(input, empty(), false);
|
||||
self.set_state_for(id, "", state);
|
||||
self.move_to(Some(id));
|
||||
}
|
||||
|
||||
/// Creates a task
|
||||
/// Creates a task with tags from filter and position
|
||||
/// Sanitizes input
|
||||
pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId {
|
||||
let (input, input_tags) = extract_tags(input.trim());
|
||||
let id = self.submit(
|
||||
self.parse_task(input.trim())
|
||||
build_task(input, input_tags, None)
|
||||
.add_tags(self.tags.iter().cloned())
|
||||
.add_tags(self.position_tags())
|
||||
.add_tags(tags.into_iter())
|
||||
);
|
||||
if set_state {
|
||||
|
@ -677,19 +683,6 @@ impl Tasks {
|
|||
id
|
||||
}
|
||||
|
||||
pub(crate) fn build_prop(
|
||||
&mut self,
|
||||
kind: Kind,
|
||||
comment: &str,
|
||||
id: EventId,
|
||||
) -> EventBuilder {
|
||||
EventBuilder::new(
|
||||
kind,
|
||||
comment,
|
||||
vec![Tag::event(id)],
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_task_title(&self, id: &EventId) -> String {
|
||||
self.tasks.get(id).map_or(id.to_string(), |t| t.get_title())
|
||||
}
|
||||
|
@ -701,7 +694,7 @@ impl Tasks {
|
|||
let stripped = str.trim().trim_start_matches('+').trim_start_matches("in ");
|
||||
if let Ok(num) = stripped.parse::<i64>() {
|
||||
self.track_at(Timestamp::from(Timestamp::now().as_u64().saturating_add_signed(num * 60)));
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
match interim::parse_date_string(stripped, Local::now(), interim::Dialect::Us) {
|
||||
Ok(date) => Some(date.to_utc()),
|
||||
|
@ -760,7 +753,12 @@ impl Tasks {
|
|||
Some(c) => { c.insert(event); }
|
||||
None => { self.history.insert(event.pubkey, BTreeSet::from([event])); }
|
||||
},
|
||||
_ => self.add_prop(&event),
|
||||
METADATA_KIND =>
|
||||
match Metadata::from_json(event.content()) {
|
||||
Ok(metadata) => { self.users.insert(event.pubkey, metadata); }
|
||||
Err(e) => warn!("Cannot parse metadata: {} from {:?}", e, event)
|
||||
}
|
||||
_ => self.add_prop(event),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -777,10 +775,17 @@ impl Tasks {
|
|||
}
|
||||
}
|
||||
|
||||
fn add_prop(&mut self, event: &Event) {
|
||||
self.referenced_tasks(&event, |t| {
|
||||
fn add_prop(&mut self, event: Event) {
|
||||
let found = self.referenced_tasks(&event, |t| {
|
||||
t.props.insert(event.clone());
|
||||
});
|
||||
if !found {
|
||||
if event.kind.as_u16() == NOTE_KIND {
|
||||
self.add_task(event);
|
||||
return;
|
||||
}
|
||||
warn!("Unknown event {:?}", event)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_own_history(&mut self) -> Option<&mut BTreeSet<Event>> {
|
||||
|
@ -812,7 +817,7 @@ impl Tasks {
|
|||
}
|
||||
|
||||
pub(crate) fn set_state_for(&mut self, id: EventId, comment: &str, state: State) -> EventId {
|
||||
let prop = self.build_prop(
|
||||
let prop = build_prop(
|
||||
state.into(),
|
||||
comment,
|
||||
id,
|
||||
|
@ -827,13 +832,19 @@ impl Tasks {
|
|||
}
|
||||
|
||||
pub(crate) fn make_note(&mut self, note: &str) {
|
||||
match self.position {
|
||||
None => warn!("Cannot add note \"{}\" without active task", note),
|
||||
Some(id) => {
|
||||
let prop = self.build_prop(Kind::TextNote, note, id);
|
||||
if let Some(id) = self.position {
|
||||
if self.get_by_id(&id).is_some_and(|t| t.is_task()) {
|
||||
let prop = build_prop(Kind::TextNote, note.trim(), id);
|
||||
self.submit(prop);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let (input, tags) = extract_tags(note.trim());
|
||||
self.submit(
|
||||
build_task(input, tags, Some(("stateless ", Kind::TextNote)))
|
||||
.add_tags(self.parent_tag())
|
||||
.add_tags(self.tags.iter().cloned())
|
||||
);
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
@ -1075,7 +1086,7 @@ mod tasks_test {
|
|||
fn test_procedures() {
|
||||
let mut tasks = stub_tasks();
|
||||
tasks.make_task_and_enter("proc: tags", State::Procedure);
|
||||
let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)]));
|
||||
let side = tasks.submit(build_task("side", vec![tasks.make_event_tag(&tasks.get_current_task().unwrap().event, MARKER_DEPENDS)], None));
|
||||
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::<EventId>::new());
|
||||
let sub_id = tasks.make_task("sub");
|
||||
assert_eq!(tasks.get_current_task().unwrap().children, HashSet::from([sub_id]));
|
||||
|
|
Loading…
Reference in New Issue