nervous-notes 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nervous_notes/__init__.py +10 -0
- nervous_notes/__main__.py +44 -0
- nervous_notes/dsl.py +63 -0
- nervous_notes/example_scripts/Example - Daily Responsibility.py +75 -0
- nervous_notes/example_scripts/Example - Extrovert.py +34 -0
- nervous_notes/example_scripts/Example - Introvert.py +54 -0
- nervous_notes/example_scripts/Example - Recurring Note Generator.py +61 -0
- nervous_notes/example_scripts/Literate Programming - Hello World.py +53 -0
- nervous_notes/example_scripts/Mqtt Publisher.py +30 -0
- nervous_notes/example_scripts/Mqtt Subscriber.py +8 -0
- nervous_notes/example_scripts/Next Notes Manager.py +25 -0
- nervous_notes/example_scripts/Next-Notes MOC.py +91 -0
- nervous_notes/example_scripts/Timer Demo.py +31 -0
- nervous_notes/nervous_note.py +144 -0
- nervous_notes/nervous_process.py +60 -0
- nervous_notes/note_companion_script.py +190 -0
- nervous_notes/note_embedded_script.py +112 -0
- nervous_notes/orchestration.py +60 -0
- nervous_notes/requirements.txt +5 -0
- nervous_notes/util.py +105 -0
- nervous_notes/vault_router.py +33 -0
- nervous_notes/wrappers/external_messages.py +127 -0
- nervous_notes/wrappers/file_watcher.py +145 -0
- nervous_notes/wrappers/note_api.py +288 -0
- nervous_notes/wrappers/scripting.py +156 -0
- nervous_notes-0.0.1.dist-info/METADATA +110 -0
- nervous_notes-0.0.1.dist-info/RECORD +29 -0
- nervous_notes-0.0.1.dist-info/WHEEL +4 -0
- nervous_notes-0.0.1.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .wrappers.external_messages import get_config_from_env
|
|
2
|
+
from .nervous_note import NervousNote
|
|
3
|
+
from .note_embedded_script import NoteEmbeddedScript
|
|
4
|
+
from .nervous_process import NervousProcess
|
|
5
|
+
from multiprocessing.connection import Connection
|
|
6
|
+
from . import dsl
|
|
7
|
+
__all__ = [
|
|
8
|
+
"NervousNote", "NoteEmbeddedScript", "NervousProcess", "get_config_from_env", "dsl",
|
|
9
|
+
"Connection", # for typing
|
|
10
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
from .orchestration import Orchestrator
|
|
5
|
+
from .nervous_note import NervousNote
|
|
6
|
+
from .wrappers.external_messages import get_config_from_env
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO,
|
|
10
|
+
format='[%(asctime)s %(pathname)s:%(lineno)s] - %(message)s',
|
|
11
|
+
datefmt='%Y-%m-%d %H:%M:%S')
|
|
12
|
+
|
|
13
|
+
parser = argparse.ArgumentParser()
|
|
14
|
+
parser.add_argument("vault_path", help="Path to the vault directory", type=str)
|
|
15
|
+
|
|
16
|
+
# FIXME: need to make sure this is properly optional, in the Orchestrator
|
|
17
|
+
parser.add_argument("--scripts", help="The directory for scripts")
|
|
18
|
+
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
mqtt_config = get_config_from_env()
|
|
22
|
+
if mqtt_config:
|
|
23
|
+
logging.info(f"Using vault {args.vault_path}, mqtt broker {mqtt_config.broker}:{mqtt_config.port} for user {mqtt_config.username}, and scripts_dir {args.scripts}")
|
|
24
|
+
else:
|
|
25
|
+
logging.info(f"Failed to get mqtt config, proceeding with None config...")
|
|
26
|
+
|
|
27
|
+
NervousNote.start_background_actors(args.vault_path, mqtt_config)
|
|
28
|
+
# FIXME: can I delete the password to let it get garbage collected?
|
|
29
|
+
|
|
30
|
+
deploy_debug_notes = False
|
|
31
|
+
# pykka.DeadLetterRouter.default_router().start() #?
|
|
32
|
+
orchestrator = Orchestrator.wake("Nervous Notes Orchestrator", args.scripts, deploy_debug_notes)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
while True:
|
|
36
|
+
input()
|
|
37
|
+
except KeyboardInterrupt:
|
|
38
|
+
logging.info("KeyboardInterrupt detected, stopping...")
|
|
39
|
+
orchestrator.stop()
|
|
40
|
+
logging.info("Orchestrator stopped")
|
|
41
|
+
NervousNote.stop_background_actors()
|
|
42
|
+
|
|
43
|
+
# FIXME: what more is needed for a proper, clean shutdown?
|
|
44
|
+
# exit(0)
|
nervous_notes/dsl.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
used for typing, not at runtime
|
|
3
|
+
|
|
4
|
+
runtime comes from scriping.CompiledScript.script_scope
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import json
|
|
9
|
+
import datetime
|
|
10
|
+
from time import ctime, sleep
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from pykka import ActorRef
|
|
14
|
+
|
|
15
|
+
from .note_companion_script import TemplateScript
|
|
16
|
+
from .util import TimeUtil, Clock
|
|
17
|
+
|
|
18
|
+
from .nervous_note import MqttWrapper
|
|
19
|
+
from .wrappers.note_api import datetimestamped_markdown_list_line, timestamped_markdown_list_line, NoteAPI
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
my_note: NoteAPI
|
|
23
|
+
note_name: str
|
|
24
|
+
topic: str
|
|
25
|
+
default_topic: str
|
|
26
|
+
mqtt: MqttWrapper
|
|
27
|
+
|
|
28
|
+
_clock = Clock()
|
|
29
|
+
_timeutils = TimeUtil(_clock)
|
|
30
|
+
next_occurrence = _timeutils.next_occurrence
|
|
31
|
+
seconds_until = _timeutils.seconds_until
|
|
32
|
+
today = _clock.today
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# may be expanded beyond template scripts
|
|
36
|
+
def wake(notename: str, template_script_name: TemplateScript) -> ActorRef: pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def set_timer(delay_seconds: float | int, payload: bytes | None = None, key: str | None = None) -> None: pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cancel_timer(key: str) -> None: pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def subscribe_internal(topic: str, subscriber) -> None: pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def publish_internal(topic: str, payload: bytes) -> None: pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def on_mqtt_message(topic: str, payload: bytes) -> None: pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def on_note_modified() -> None: pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def on_start() -> None: pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def on_timer(key: str, payload: object) -> None: pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def publish_to_ntfy(channel: str, message: str) -> None: pass
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
if TYPE_CHECKING: # IDE support
|
|
2
|
+
from ..dsl import *
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def on_start():
|
|
6
|
+
reset_timer_from_note() # THERE's ALWAYS _a_ TIMER, for later today or tomorrow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def on_note_modified():
|
|
10
|
+
note = my_note.note_if_markdown_starts_with_pressed_button()
|
|
11
|
+
if note:
|
|
12
|
+
_frontmatter, markdown = note
|
|
13
|
+
sleep(0.25)
|
|
14
|
+
if today().isoformat() not in markdown:
|
|
15
|
+
updated_markdown = markdown_list_prepender(markdown)
|
|
16
|
+
reset_timer_from_note(note)
|
|
17
|
+
my_note.set_markdown(updated_markdown)
|
|
18
|
+
else:
|
|
19
|
+
my_note.reset_button_at_start_of_markdown()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def on_timer(_key, _payload):
|
|
23
|
+
note = frontmatter, _markdown = my_note.get_note()
|
|
24
|
+
channel = frontmatter.get("channel")
|
|
25
|
+
message = frontmatter.get("message")
|
|
26
|
+
logging.info(f"Timer called, publishing to {channel}: {message}")
|
|
27
|
+
publish_to_ntfy(channel, message)
|
|
28
|
+
reset_timer_from_note(note)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# utils
|
|
32
|
+
|
|
33
|
+
def markdown_list_prepender(markdown: str) -> str:
|
|
34
|
+
"""
|
|
35
|
+
- reset the button
|
|
36
|
+
- prepend today's date (after the button)
|
|
37
|
+
"""
|
|
38
|
+
lines = markdown.splitlines()
|
|
39
|
+
lines[0] = "- [ ] Mark as done"
|
|
40
|
+
line_to_prepend = f"- [[{my_note.note_name} ({today().isoformat()})]]"
|
|
41
|
+
lines.insert(1, line_to_prepend)
|
|
42
|
+
lines[-1] = lines[-1] + "\n"
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def reset_timer_from_note(note=None):
|
|
47
|
+
"""
|
|
48
|
+
resets the timer for later today or tomorrow
|
|
49
|
+
"""
|
|
50
|
+
if note is None:
|
|
51
|
+
note = my_note.get_note()
|
|
52
|
+
|
|
53
|
+
if note is not None:
|
|
54
|
+
frontmatter, markdown = note
|
|
55
|
+
|
|
56
|
+
next_notification = next_occurrence(frontmatter.get("ifNotDoneBy"))
|
|
57
|
+
rightnows_today = today()
|
|
58
|
+
|
|
59
|
+
today_marked_as_done = rightnows_today.isoformat() in markdown
|
|
60
|
+
next_notification_is_for_today = ((rightnows_today.year, rightnows_today.month, rightnows_today.day) ==
|
|
61
|
+
(next_notification.year, next_notification.month, next_notification.day))
|
|
62
|
+
|
|
63
|
+
next_notification_already_done = today_marked_as_done and next_notification_is_for_today
|
|
64
|
+
if next_notification_already_done:
|
|
65
|
+
next_notification += datetime.timedelta(days=1)
|
|
66
|
+
|
|
67
|
+
delay = seconds_until(next_notification)
|
|
68
|
+
logging.info(f"Setting timer for {next_notification}, delay {delay}s")
|
|
69
|
+
set_timer(
|
|
70
|
+
delay,
|
|
71
|
+
None, # no payload for this timer
|
|
72
|
+
key=f"{my_note.note_name}/TIMER" # there's just one timer
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
logging.warning("No note; need yaml for {channel, message, ifNotDoneBy} (e.g. ifNotDoneBy = 20:32:00-07:00)")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
def on_start():
|
|
2
|
+
my_note.set_file_contents(f"""---
|
|
3
|
+
in_topic: {default_topic}
|
|
4
|
+
out_topic: {default_topic}/publish
|
|
5
|
+
message_to_send: "Hello :)"
|
|
6
|
+
---
|
|
7
|
+
- [ ] Send message
|
|
8
|
+
- Instructions:
|
|
9
|
+
- `out_topic` is updated live, but `in_topic` is currently provided but not read back
|
|
10
|
+
- ---
|
|
11
|
+
""")
|
|
12
|
+
|
|
13
|
+
def on_mqtt_message(topic, message):
|
|
14
|
+
my_note.append_datetimestamped_markdown_list_line(f"Received on {topic} message: {message.decode()}")
|
|
15
|
+
|
|
16
|
+
def on_note_modified():
|
|
17
|
+
if (maybe_note := my_note.note_if_markdown_starts_with_pressed_button()) is not None:
|
|
18
|
+
frontmatter, markdown = maybe_note
|
|
19
|
+
else:
|
|
20
|
+
return # not a note update we care about
|
|
21
|
+
|
|
22
|
+
if (out_topic := frontmatter.get("out_topic")) is None:
|
|
23
|
+
logging.warning("Button pressed but Frontmatter out_topic was empty")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
if (message_to_send := frontmatter.get("message_to_send")) is None:
|
|
27
|
+
logging.warning("Button pressed but Frontmatter message_to_send was empty")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# introvert expects `(sender)> (message)` where sender is who they will reply to
|
|
31
|
+
mqtt.publish(out_topic, f"{topic}> {message_to_send}")
|
|
32
|
+
|
|
33
|
+
sleep(.25) # hack so Obsidian sees the update (otherwise it seems to miss it sometimes)
|
|
34
|
+
my_note.reset_button_at_start_of_markdown()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
def on_start():
|
|
2
|
+
my_note.set_file_contents(f"""---
|
|
3
|
+
in_topic: {default_topic}
|
|
4
|
+
reply_with: "👋"
|
|
5
|
+
---
|
|
6
|
+
- [ ] Pause replying
|
|
7
|
+
- Instructions:
|
|
8
|
+
- Change `reply_with` to whatever you would like (`in_topic` does not read back updates currently)
|
|
9
|
+
- delay_seconds is an optional field, assumed to be 0 (seconds)
|
|
10
|
+
- ---
|
|
11
|
+
""")
|
|
12
|
+
|
|
13
|
+
sending = True
|
|
14
|
+
|
|
15
|
+
def on_mqtt_message(topic, message):
|
|
16
|
+
config = my_note.get_frontmatter()
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
# expects:
|
|
20
|
+
# sending_topic> message
|
|
21
|
+
sender, what_they_sent = message.decode().split(">", 1)
|
|
22
|
+
except ValueError:
|
|
23
|
+
my_note.append_datetimestamped_markdown_list_line(f"Received a weirdly formatted message: {message}")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
if not sending:
|
|
27
|
+
my_note.append_datetimestamped_markdown_list_line(f"{sender} sent {what_they_sent}, not replying (sending paused)")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
delay_seconds = config.get("delay_seconds", 0)
|
|
31
|
+
|
|
32
|
+
if (reply_with := config.get("reply_with")) is not None:
|
|
33
|
+
if delay_seconds:
|
|
34
|
+
set_timer(delay_seconds, (sender, reply_with))
|
|
35
|
+
my_note.append_datetimestamped_markdown_list_line(f"Received {what_they_sent}, replying with {reply_with} after {delay_seconds}s")
|
|
36
|
+
else:
|
|
37
|
+
my_note.append_datetimestamped_markdown_list_line(f"Received {what_they_sent}, replying with {reply_with}")
|
|
38
|
+
mqtt.publish(sender, reply_with)
|
|
39
|
+
else:
|
|
40
|
+
my_note.append_datetimestamped_markdown_list_line(f"{sender} sent {what_they_sent}, not replying (no reply_with frontmatter)")
|
|
41
|
+
|
|
42
|
+
def on_timer(key, payload):
|
|
43
|
+
sender, reply_with = payload
|
|
44
|
+
mqtt.publish(sender, reply_with)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def on_note_modified():
|
|
48
|
+
global sending # nonlocal doesn't work because sending is not enclosed in a function
|
|
49
|
+
|
|
50
|
+
maybe_note = my_note.note_if_markdown_starts_with_pressed_button()
|
|
51
|
+
if maybe_note is not None:
|
|
52
|
+
sending = False
|
|
53
|
+
else:
|
|
54
|
+
sending = True
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from typing import io
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING: # IDE support
|
|
5
|
+
from dsl import *
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Frontmatter:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Document:
|
|
13
|
+
frontmatter: Frontmatter
|
|
14
|
+
markdown: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def on_start():
|
|
18
|
+
note = my_note.get_note()
|
|
19
|
+
if note is not None:
|
|
20
|
+
frontmatter, markdown = note
|
|
21
|
+
|
|
22
|
+
next_date = frontmatter.get("next_date")
|
|
23
|
+
default_interval_days = frontmatter.get("default_interval_days")
|
|
24
|
+
|
|
25
|
+
_button, current_next_note, *history = markdown.splitlines()
|
|
26
|
+
|
|
27
|
+
if current_next_note[:8] != "- Next: " or not current_next_note.endswith(")]]"):
|
|
28
|
+
logging.warning(f"Expected second line to start with `- Next: ` then a wikilink of the format [[Note (YYYY-MM-DD)]]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
current_next_date = current_next_note[-13:-3]
|
|
32
|
+
if next_date is None:
|
|
33
|
+
next_date = datetime.datetime.strptime(current_next_date, "%Y-%m-%d") + timedelta(days=default_interval_days)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def on_note_modified():
|
|
37
|
+
note = my_note.note_if_markdown_starts_with_pressed_button()
|
|
38
|
+
if note:
|
|
39
|
+
frontmatter, markdown = note
|
|
40
|
+
# FIXME
|
|
41
|
+
stream = io.StringIO()
|
|
42
|
+
my_note.yaml.dump(data, stream)
|
|
43
|
+
|
|
44
|
+
# Extract the YAML string
|
|
45
|
+
yaml_string = stream.getvalue()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# utils
|
|
49
|
+
|
|
50
|
+
# FIXME: factor out?
|
|
51
|
+
def markdown_list_prepender(markdown: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
- reset the button
|
|
54
|
+
- prepend today's date (after the button)
|
|
55
|
+
"""
|
|
56
|
+
lines = markdown.splitlines()
|
|
57
|
+
lines[0] = "- [ ] Mark as done"
|
|
58
|
+
line_to_prepend = f"- [[{my_note.note_name} ({datetime.date.today().isoformat()})]]"
|
|
59
|
+
lines.insert(1, line_to_prepend)
|
|
60
|
+
lines[-1] = lines[-1] + "\n"
|
|
61
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
def on_start():
|
|
2
|
+
my_note.set_file_contents(f"""---
|
|
3
|
+
in_topic: {default_topic}
|
|
4
|
+
out_topic: {default_topic}/publish
|
|
5
|
+
message_to_send: "Hello :)"
|
|
6
|
+
---
|
|
7
|
+
- [ ] Send message
|
|
8
|
+
- Instructions:
|
|
9
|
+
- press the button above to send `message_to_send` to `out_topic`
|
|
10
|
+
- (this can be observed via mosquitto_pub)
|
|
11
|
+
- send a message to `in_topic` and observe it listed below
|
|
12
|
+
- `out_topic` is read dynamically but `in_topic` is provided for informational purposes only
|
|
13
|
+
- ---
|
|
14
|
+
""")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def on_mqtt_message(topic, message):
|
|
18
|
+
my_note.append_datetimestamped_markdown_list_line(f"received on `{topic}`: {message}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def on_note_modified():
|
|
22
|
+
if (maybe_note := my_note.note_if_markdown_starts_with_pressed_button()) is not None:
|
|
23
|
+
frontmatter, markdown = maybe_note
|
|
24
|
+
else:
|
|
25
|
+
logging.debug("Note modified but button not pressed")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if (out_topic := frontmatter.get("out_topic")) is None:
|
|
29
|
+
logging.warning("Button pressed but Frontmatter out_topic was empty")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
if (message_to_send := frontmatter.get("message_to_send")) is None:
|
|
33
|
+
logging.warning("Button pressed but Frontmatter message_to_send was empty")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
logging.info(f"[{note_name}] publishing {message_to_send} to mqtt")
|
|
37
|
+
mqtt.publish(out_topic, message_to_send)
|
|
38
|
+
|
|
39
|
+
logging.info(f"[{note_name}] resetting markdown...")
|
|
40
|
+
|
|
41
|
+
sleep(.25) # hack to prevent a race condition with Obsidian's display
|
|
42
|
+
my_note.upsert_markdown(lambda markdown: upserter(out_topic, message_to_send, markdown))
|
|
43
|
+
logging.info(f"[{note_name}] Markdown reset")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# util
|
|
47
|
+
|
|
48
|
+
def upserter(out_topic, message_to_send, markdown):
|
|
49
|
+
lines = markdown.splitlines()
|
|
50
|
+
lines[0] = f"- [ ] Send (last sent ~{ctime()})"
|
|
51
|
+
lines.append(timestamped_markdown_list_line(f"sending on `{out_topic}`: {message_to_send}"))
|
|
52
|
+
|
|
53
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
def on_start():
|
|
2
|
+
my_note.set_file_contents(f"""---
|
|
3
|
+
out_topic: "(fill in)"
|
|
4
|
+
message_to_send: "hello"
|
|
5
|
+
---
|
|
6
|
+
- [ ] Send
|
|
7
|
+
""")
|
|
8
|
+
|
|
9
|
+
def on_mqtt_message(topic, message):
|
|
10
|
+
my_note.append_datetimestamped_markdown_list_line(f"(ignoring) received on `{topic}`: {message}")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def on_note_modified():
|
|
14
|
+
if (maybe_note := my_note.note_if_markdown_starts_with_pressed_button()) is not None:
|
|
15
|
+
frontmatter, markdown = maybe_note
|
|
16
|
+
else:
|
|
17
|
+
return # not a note update we care about
|
|
18
|
+
|
|
19
|
+
if (out_topic := frontmatter.get("out_topic")) is None:
|
|
20
|
+
logging.warning("Button pressed but Frontmatter out_topic was empty")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
if (message_to_send := frontmatter.get("message_to_send")) is None:
|
|
24
|
+
logging.warning("Button pressed but Frontmatter message_to_send was empty")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
mqtt.publish(out_topic, message_to_send)
|
|
28
|
+
|
|
29
|
+
sleep(.25) # hack so Obsidian sees the update (otherwise it seems to miss it sometimes)
|
|
30
|
+
my_note.reset_button_at_start_of_markdown()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
if TYPE_CHECKING: # IDE support
|
|
2
|
+
from ..dsl import *
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def on_start():
|
|
6
|
+
frontmatter, markdown = my_note.get_note()
|
|
7
|
+
|
|
8
|
+
lines = markdown.strip().split('\n')
|
|
9
|
+
|
|
10
|
+
failed_matches = []
|
|
11
|
+
for line in lines:
|
|
12
|
+
if not line.startswith('- [[') or not line.endswith(']]'):
|
|
13
|
+
failed_matches.append(line)
|
|
14
|
+
|
|
15
|
+
if failed_matches:
|
|
16
|
+
logging.warning(f"Failed matches! {failed_matches} (lines: {lines})")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
template_to_use = frontmatter.get("template_to_use", "Next-Notes MOC")
|
|
20
|
+
logging.info(f"Using template: {template_to_use}")
|
|
21
|
+
|
|
22
|
+
next_notes = [line[4:-2] for line in lines]
|
|
23
|
+
for next_note in next_notes:
|
|
24
|
+
logging.info(f"waking [[{next_note}]]")
|
|
25
|
+
wake(next_note, TemplateScript(template_to_use))
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING: # IDE support
|
|
4
|
+
from ..dsl import *
|
|
5
|
+
|
|
6
|
+
DEFAULT_INTERVAL_DAYS = 7
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def on_start():
|
|
10
|
+
if my_note.already_exists():
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
today_today = today()
|
|
14
|
+
my_note.set_file_contents(f"""---
|
|
15
|
+
default_interval_days: {DEFAULT_INTERVAL_DAYS}
|
|
16
|
+
---
|
|
17
|
+
- [ ] Create next: [[{my_note.note_name} ({today_today.isoformat()})]]
|
|
18
|
+
|
|
19
|
+
## History
|
|
20
|
+
|
|
21
|
+
- [[{my_note.note_name} ({(today_today + timedelta(days=-DEFAULT_INTERVAL_DAYS)).isoformat()})]]
|
|
22
|
+
- [[{my_note.note_name} ({(today_today + timedelta(days=-DEFAULT_INTERVAL_DAYS*2)).isoformat()})]]
|
|
23
|
+
- (anything from the line above downward is basically ignored)
|
|
24
|
+
- (once you create one real link, e.g. with the button, you can delete these extra lines with no problem)
|
|
25
|
+
""")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def on_note_modified():
|
|
29
|
+
logging.info("[Next-Notes Moc.py:on_note_modified] CANARY")
|
|
30
|
+
note = my_note.note_if_markdown_starts_with_pressed_button()
|
|
31
|
+
if note:
|
|
32
|
+
frontmatter, markdown = note
|
|
33
|
+
|
|
34
|
+
logging.info("Button pressed! ...")
|
|
35
|
+
|
|
36
|
+
lines = markdown.split("\n")
|
|
37
|
+
button = lines[0]
|
|
38
|
+
|
|
39
|
+
# The next note to create is named by the wikilink in the button line,
|
|
40
|
+
# e.g. "- [x] Create next: [[Next Notes Mockup (2026-07-19)]]".
|
|
41
|
+
next_target = _wikilink_target(button)
|
|
42
|
+
|
|
43
|
+
logging.warning(
|
|
44
|
+
f"The user needs to manually create: a calendar event(?) and [[{next_target}]]. Any Next* aliasing has also been left untouched."
|
|
45
|
+
)
|
|
46
|
+
# FIXME: here I can wake() the next target, if only to move the alias along... how to model that?
|
|
47
|
+
|
|
48
|
+
# Advance the button to the note after this one, keeping the label text.
|
|
49
|
+
interval_days = frontmatter.get("default_interval_days", 7)
|
|
50
|
+
following_target = _bump_target(next_target, interval_days)
|
|
51
|
+
label = button[len("- [x] "):button.index("[[")]
|
|
52
|
+
lines[0] = f"- [ ] {label}[[{following_target}]]"
|
|
53
|
+
|
|
54
|
+
# Record the just-created note at the top of History, if not already there.
|
|
55
|
+
history_line = f"- [[{next_target}]]"
|
|
56
|
+
if history_line not in lines:
|
|
57
|
+
sleep(0.25)
|
|
58
|
+
_prepend_to_history(lines, history_line)
|
|
59
|
+
|
|
60
|
+
my_note.set_markdown("\n".join(lines))
|
|
61
|
+
logging.info("Done, setting markdown...")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# utils
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _wikilink_target(text: str) -> str:
|
|
68
|
+
"""Return the NAME inside the first [[NAME]] wikilink in `text`."""
|
|
69
|
+
start = text.index("[[") + 2
|
|
70
|
+
end = text.index("]]", start)
|
|
71
|
+
return text[start:end]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _bump_target(target: str, interval_days: int) -> str:
|
|
75
|
+
""""Base (YYYY-MM-DD)" -> same base with the date advanced `interval_days`."""
|
|
76
|
+
base = target[:target.rindex(" (")]
|
|
77
|
+
date_str = target[target.rindex("(") + 1:target.rindex(")")]
|
|
78
|
+
date = datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
79
|
+
following = date + datetime.timedelta(days=interval_days)
|
|
80
|
+
return f"{base} ({following.isoformat()})"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _prepend_to_history(lines: list, history_line: str) -> None:
|
|
84
|
+
"""Insert `history_line` before the first existing History entry (or append)."""
|
|
85
|
+
header_index = lines.index("## History")
|
|
86
|
+
insert_at = len(lines)
|
|
87
|
+
for i in range(header_index + 1, len(lines)):
|
|
88
|
+
if lines[i].startswith("- [["):
|
|
89
|
+
insert_at = i
|
|
90
|
+
break
|
|
91
|
+
lines.insert(insert_at, history_line)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
def on_start():
|
|
2
|
+
my_note.set_file_contents(f"""---
|
|
3
|
+
duration_seconds: 1
|
|
4
|
+
---
|
|
5
|
+
- [ ] Start timer
|
|
6
|
+
""")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def on_note_modified():
|
|
10
|
+
if (maybe_note := my_note.note_if_markdown_starts_with_pressed_button()) is not None:
|
|
11
|
+
frontmatter, markdown = maybe_note
|
|
12
|
+
else:
|
|
13
|
+
return # not a note update we care about
|
|
14
|
+
|
|
15
|
+
if (duration_seconds := frontmatter.get("duration_seconds")) is None:
|
|
16
|
+
logging.warning("Button pressed but Frontmatter duration_seconds was empty")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
set_timer(duration_seconds, duration_seconds)
|
|
20
|
+
sleep(.25) # hack so Obsidian sees the update (otherwise it seems to miss it sometimes)
|
|
21
|
+
my_note.upsert_markdown(lambda markdown: upserter(duration_seconds, markdown))
|
|
22
|
+
|
|
23
|
+
def on_timer(key, duration_seconds):
|
|
24
|
+
my_note.append_datetimestamped_markdown_list_line(f"Finished {duration_seconds}s timer")
|
|
25
|
+
|
|
26
|
+
def upserter(duration_seconds, markdown):
|
|
27
|
+
lines = markdown.splitlines()
|
|
28
|
+
lines[0] = f"- [ ] Start timer"
|
|
29
|
+
lines.append(f"- \\[{ctime()}] Just set a timer for {duration_seconds}s\n")
|
|
30
|
+
|
|
31
|
+
return "\n".join(lines)
|