diff --git a/.config/config.ron b/.config/config.ron index 52270bc..a515b61 100644 --- a/.config/config.ron +++ b/.config/config.ron @@ -1,10 +1,10 @@ ( directory: "/Users/suri/dev/projects/filaments/ZettelKasten", global_key_binds: { + "down": MoveDown, "up": MoveUp, "ctrl-c": Quit, "ctrl-z": Suspend, - "down": MoveDown, }, zk: ( keybinds: { @@ -18,57 +18,65 @@ todo: ( explorer: ( keybinds: { + "2": SwitchTo( + page: Todo(Inspector), + ), "g": NewSubGroup, "tab": SwitchTo( page: Zk, ), - "2": SwitchTo( - page: Todo(Inspector), + "shift-g": NewGroup, + "j": MoveDown, + "k": MoveUp, + "1": SwitchTo( + page: Todo(Explorer), ), "3": SwitchTo( page: Todo(TaskList), ), - "1": SwitchTo( - page: Todo(Explorer), - ), - "k": MoveUp, "t": NewTask, - "shift-g": NewGroup, - "j": MoveDown, + "enter": SwitchTo( + page: Todo(Inspector), + ), }, ), inspector: ( keybinds: { - "tab": SwitchTo( - page: Zk, - ), "1": SwitchTo( page: Todo(Explorer), ), - "3": SwitchTo( - page: Todo(TaskList), + "tab": SwitchTo( + page: Zk, ), "2": SwitchTo( page: Todo(Inspector), ), + "3": SwitchTo( + page: Todo(TaskList), + ), + "n": EditName, + "p": EditPriority, }, ), tasklist: ( keybinds: { - "1": SwitchTo( - page: Todo(Explorer), + "k": MoveUp, + "enter": SwitchTo( + page: Todo(Inspector), ), "tab": SwitchTo( page: Zk, ), - "j": MoveDown, - "k": MoveUp, + "1": SwitchTo( + page: Todo(Explorer), + ), "2": SwitchTo( page: Todo(Inspector), ), "3": SwitchTo( page: Todo(TaskList), ), + "j": MoveDown, }, ), ), diff --git a/.config/default_config.ron b/.config/default_config.ron index 0f8904b..247be20 100644 --- a/.config/default_config.ron +++ b/.config/default_config.ron @@ -25,6 +25,7 @@ "shift-g": NewGroup, "g": NewSubGroup, "t": NewTask, + "enter": SwitchTo(page: Todo(Inspector)) }, ), inspector: ( @@ -33,6 +34,8 @@ "1": SwitchTo(page: Todo(Explorer)), "2": SwitchTo(page: Todo(Inspector)), "3": SwitchTo(page: Todo(TaskList)), + "n": EditName, + "p": EditPriority }, ), tasklist: ( @@ -43,6 +46,7 @@ "3": SwitchTo(page: Todo(TaskList)), "j": MoveDown, "k": MoveUp, + "enter": SwitchTo(page: Todo(Inspector)) }, ), ), diff --git a/justfile b/justfile index 6966c6a..7cad845 100644 --- a/justfile +++ b/justfile @@ -22,6 +22,7 @@ test: reset: rm -rf ZettleKasten cargo run -- init + cargo run # Only used to build / generate entities dev-db := justfile_directory() + "/target/dev.db" diff --git a/src/tui/app.rs b/src/tui/app.rs index f7e3bf9..e94a9c3 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -32,6 +32,7 @@ pub struct App { signal_tx: UnboundedSender, signal_rx: UnboundedReceiver, viz_signal_tx: UnboundedSender, + raw_text: bool, } /// The different regions of the application that the user can @@ -73,6 +74,7 @@ impl App { signal_tx, signal_rx, viz_signal_tx, + raw_text: false, }) } @@ -151,6 +153,11 @@ impl App { fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { debug!("key received: {key:#?}"); + if self.raw_text { + debug!("Raw text enabled, refusing to interpret as Signal"); + return Ok(()); + } + let signal_tx = self.signal_tx.clone(); let Some(page_keymap) = self.config.keymap.get(&self.page) else { @@ -244,6 +251,9 @@ impl App { Signal::ClearScreen => tui.terminal.clear()?, Signal::Resize(x, y) => self.handle_resize(tui, x, y)?, Signal::Render => self.render(tui)?, + + Signal::EnterRawText => self.raw_text = true, + Signal::ExitRawText => self.raw_text = false, _ => {} } diff --git a/src/tui/components/todo/inspector/groupview.rs b/src/tui/components/todo/inspector/groupview.rs index d2e8129..5d4e709 100644 --- a/src/tui/components/todo/inspector/groupview.rs +++ b/src/tui/components/todo/inspector/groupview.rs @@ -9,8 +9,8 @@ use crate::types::Group; #[derive(Debug, Clone)] pub struct GroupView<'text> { - name: TextArea<'text>, - priority: TextArea<'text>, + pub name: TextArea<'text>, + pub priority: TextArea<'text>, created_at: Paragraph<'text>, layouts: Layouts, } diff --git a/src/tui/components/todo/inspector/mod.rs b/src/tui/components/todo/inspector/mod.rs index 2c712ba..5c4e7f1 100644 --- a/src/tui/components/todo/inspector/mod.rs +++ b/src/tui/components/todo/inspector/mod.rs @@ -1,10 +1,22 @@ +use async_trait::async_trait; +use crossterm::event::{KeyCode, KeyEvent}; +use dto::{GroupEntity, IntoActiveModel, NanoId, TagEntity, TaskEntity}; use ratatui::{ - layout::{Constraint, Direction, Layout}, + Frame, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, - widgets::{Block, BorderType, Borders, Widget}, + widgets::{Block, BorderType, Borders}, }; +use ratatui_textarea::CursorMove; +use tokio::sync::mpsc::UnboundedSender; -use crate::types::{TodoNode, TodoNodeKind}; +use crate::{ + tui::{ + Signal, + components::{Component, DEFAULT_NAME}, + }, + types::{KastenHandle, TodoNode, TodoNodeKind}, +}; mod rootview; use rootview::RootView; @@ -16,13 +28,72 @@ mod groupview; use groupview::GroupView; pub struct Inspector<'text> { - render_data: RenderData<'text>, + pub render_data: RenderData<'text>, margins: Layout, block: Block<'text>, + kh: KastenHandle, + signal_tx: Option>, + is_active: bool, + editing: Option, + // this is the `NanoId` of the thing we are actually inspecting + inspecting: Option, +} + +enum Edit { + Name, + Priority, } impl Inspector<'_> { + pub fn new(kh: KastenHandle, node: &TodoNode) -> Self { + let margins = Layout::new(Direction::Horizontal, [Constraint::Percentage(100)]) + .horizontal_margin(3) + .vertical_margin(2); + + let block = Block::new() + .title("[2]") + .title("Inspector") + .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) + .border_style(Style::new().fg(Color::Gray)) + .border_type(BorderType::Rounded); + + let mut nanoid = None; + + let render_data = match node.kind { + TodoNodeKind::Root => RenderData::Root { + widget: Box::new(RootView::default()), + }, + TodoNodeKind::Group(ref group) => { + nanoid = Some(group.id.clone()); + + RenderData::Group { + widget: Box::new(GroupView::from(&**group)), + } + } + TodoNodeKind::Task(ref task) => { + nanoid = Some(task.id.clone()); + + RenderData::Task { + widget: Box::new(TaskView::from(&**task)), + } + } + }; + + Self { + render_data, + margins, + block, + kh, + is_active: false, + editing: None, + inspecting: nanoid, + signal_tx: None, + } + } + pub fn set_active(&mut self) { + self.is_active = true; + self.block = Block::new() .title("[2]") .title("Inspector") @@ -32,6 +103,7 @@ impl Inspector<'_> { } pub fn set_inactive(&mut self) { + self.is_active = false; self.block = Block::new() .title("[2]") .title("Inspector") @@ -39,65 +111,224 @@ impl Inspector<'_> { .border_style(Style::new().fg(Color::Gray)) .border_type(BorderType::Rounded); } + + pub fn inspect(&mut self, node: &TodoNode) { + self.render_data = match node.kind { + TodoNodeKind::Root => { + self.inspecting = None; + RenderData::Root { + widget: Box::new(RootView::default()), + } + } + TodoNodeKind::Group(ref group) => { + self.inspecting = Some(group.id.clone()); + RenderData::Group { + widget: Box::new(GroupView::from(&**group)), + } + } + TodoNodeKind::Task(ref task) => { + self.inspecting = Some(task.id.clone()); + RenderData::Task { + widget: Box::new(TaskView::from(&**task)), + } + } + } + } } -enum RenderData<'text> { +pub enum RenderData<'text> { Root { widget: Box> }, Task { widget: Box> }, Group { widget: Box> }, } -impl From<&TodoNode> for Inspector<'_> { - fn from(value: &TodoNode) -> Self { - let margins = Layout::new(Direction::Horizontal, [Constraint::Percentage(100)]) - .horizontal_margin(3) - .vertical_margin(2); +#[async_trait] +impl Component for Inspector<'_> { + fn register_signal_handler(&mut self, tx: UnboundedSender) -> color_eyre::Result<()> { + self.signal_tx = Some(tx); + Ok(()) + } - let block = Block::new() - .title("[2]") - .title("Inspector") - .borders(Borders::LEFT | Borders::TOP | Borders::BOTTOM) - .border_style(Style::new().fg(Color::Gray)) - .border_type(BorderType::Rounded); - match value.kind { - TodoNodeKind::Root => Self { - render_data: RenderData::Root { - widget: Box::new(RootView::default()), - }, - margins, - block, - }, - TodoNodeKind::Group(ref group) => Self { - render_data: RenderData::Group { - widget: Box::new(GroupView::from(&**group)), - }, - margins, - block, - }, - TodoNodeKind::Task(ref task) => Self { - render_data: RenderData::Task { - widget: Box::new(TaskView::from(&**task)), - }, - margins, - block, - }, + #[allow(clippy::too_many_lines)] + async fn update(&mut self, signal: Signal) -> color_eyre::Result> { + match signal { + Signal::EditName => { + let name = match &mut self.render_data { + RenderData::Root { widget: _ } => return Ok(None), + RenderData::Task { widget } => &mut widget.name, + RenderData::Group { widget } => &mut widget.name, + }; + + name.set_block( + name.block() + .cloned() + .expect("All of them should have blocks") + .border_style(Style::default().fg(Color::Green)), + ); + + if name.lines()[0].as_str().contains(DEFAULT_NAME) { + name.delete_line_by_end(); + } else { + name.move_cursor(CursorMove::End); + } + + name.set_cursor_style(Style::default().reversed()); + name.set_cursor_line_style(Style::default().underlined()); + + self.editing = Some(Edit::Name); + return Ok(Some(Signal::EnterRawText)); + } + Signal::EditPriority => { + let priority = match &mut self.render_data { + RenderData::Root { widget: _ } => return Ok(None), + RenderData::Task { widget } => &mut widget.priority, + RenderData::Group { widget } => &mut widget.priority, + }; + + priority.set_block( + priority + .block() + .cloned() + .expect("All of them should have blocks") + .border_style(Style::default().fg(Color::Green)), + ); + + priority.set_cursor_style(Style::default().reversed()); + priority.set_cursor_line_style(Style::default().underlined()); + + self.editing = Some(Edit::Priority); + return Ok(Some(Signal::EnterRawText)); + } + + _ => {} } + Ok(None) } -} -impl Widget for &Inspector<'_> { - fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) - where - Self: Sized, - { - self.block.clone().render(area, buf); + #[allow(clippy::too_many_lines)] + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result> { + let signal_tx = self + .signal_tx + .as_mut() + .expect("Invariant Broken, signal_tx must be initialized"); + + match self.editing { + Some(Edit::Name) => { + let name = match &mut self.render_data { + RenderData::Root { widget: _ } => return Ok(None), + RenderData::Task { widget } => &mut widget.name, + RenderData::Group { widget } => &mut widget.name, + }; + + if key.code == KeyCode::Enter { + name.set_cursor_style(Style::reset()); + name.set_cursor_line_style(Style::reset()); + name.set_block( + name.block() + .cloned() + .expect("All of them should have blocks") + .border_style(Style::default().fg(Color::Reset)), + ); + self.editing = None; + signal_tx.send(Signal::ExitRawText)?; + + let new_name = name.lines()[0].clone(); + let id = self + .inspecting + .clone() + .expect("Invariant Broken, this must be some id"); + + let kt = self.kh.read().await; + match &self.render_data { + RenderData::Task { .. } => { + let _ = TaskEntity::load() + .filter_by_nano_id(id.clone()) + .one(&kt.db) + .await? + .expect("Invariant Broken: Must exist") + .into_active_model() + .set_name(new_name.as_str()) + .save(&kt.db) + .await?; + } + RenderData::Group { .. } => { + let g = GroupEntity::load() + .filter_by_nano_id(id.clone()) + .with(TagEntity) + .one(&kt.db) + .await? + .expect("Invariant Broken: Must exist"); + let tag_id = g.tag.as_ref().expect("Must be loaded").nano_id.clone(); + + let _ = g + .into_active_model() + .set_name(new_name.as_str()) + .save(&kt.db) + .await?; + + TagEntity::load() + .filter_by_nano_id(tag_id) + .one(&kt.db) + .await? + .expect("Invariant Broken: Must exist") + .into_active_model() + .set_name(new_name.as_str()) + .save(&kt.db) + .await?; + } + RenderData::Root { .. } => unreachable!("Already returned above"), + } + + drop(kt); + + Ok(Some(Signal::Refresh)) + } else { + name.input_without_shortcuts(key); + Ok(None) + } + } + Some(Edit::Priority) => { + let priority = match &mut self.render_data { + RenderData::Root { widget: _ } => return Ok(None), + RenderData::Task { widget } => &mut widget.priority, + RenderData::Group { widget } => &mut widget.priority, + }; + + if key.code == KeyCode::Enter { + priority.set_cursor_style(Style::reset()); + priority.set_cursor_line_style(Style::reset()); + + priority.set_block( + priority + .block() + .cloned() + .expect("All of them should have blocks") + .border_style(Style::default().fg(Color::Reset)), + ); + + self.editing = None; + Ok(Some(Signal::ExitRawText)) + } else { + priority.input_without_shortcuts(key); + Ok(None) + } + } + + None => return Ok(None), + } + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { + frame.render_widget(self.block.clone(), area); let area = self.margins.split(area)[0]; match &self.render_data { - RenderData::Root { widget } => widget.clone().render(area, buf), - RenderData::Task { widget } => widget.clone().render(area, buf), - RenderData::Group { widget } => widget.clone().render(area, buf), + RenderData::Root { widget } => frame.render_widget(*widget.clone(), area), + RenderData::Task { widget } => frame.render_widget(*widget.clone(), area), + RenderData::Group { widget } => frame.render_widget(*widget.clone(), area), } + + Ok(()) } } diff --git a/src/tui/components/todo/inspector/taskview.rs b/src/tui/components/todo/inspector/taskview.rs index acff83d..af57e21 100644 --- a/src/tui/components/todo/inspector/taskview.rs +++ b/src/tui/components/todo/inspector/taskview.rs @@ -9,8 +9,8 @@ use crate::types::Task; #[derive(Debug, Clone)] pub struct TaskView<'text> { - name: TextArea<'text>, - priority: TextArea<'text>, + pub name: TextArea<'text>, + pub priority: TextArea<'text>, parent_group: Paragraph<'text>, due_finished_at: Paragraph<'text>, layouts: Layouts, diff --git a/src/tui/components/todo/mod.rs b/src/tui/components/todo/mod.rs index 2bfc125..ae9b99d 100644 --- a/src/tui/components/todo/mod.rs +++ b/src/tui/components/todo/mod.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; -use dto::NanoId; +use color_eyre::eyre::Result; +use crossterm::event::KeyEvent; use ratatui::{ Frame, layout::{Constraint, Layout, Rect, Size}, @@ -12,7 +13,7 @@ use tracing::debug; use crate::{ tui::{Page, Signal, components::Component}, - types::{Group, KastenHandle, Priority, Task}, + types::{Group, KastenHandle, Priority, Task, TodoTree}, }; mod explorer; @@ -24,7 +25,6 @@ mod inspector; use inspector::Inspector; pub struct Todo<'text> { - #[expect(dead_code)] signal_tx: Option>, kh: KastenHandle, layouts: Layouts, @@ -37,6 +37,8 @@ pub struct Todo<'text> { active: TodoRegion, } +pub const DEFAULT_NAME: &str = "Rename Me!"; + /// The different regions inside the `Todo` component #[derive( Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter, Display, @@ -80,7 +82,12 @@ impl Todo<'_> { .state .selected() .and_then(|idx| task_list.id_list.get(idx)); - let kt = self.kh.read().await; + + let mut kt = self.kh.write().await; + + // fuck it we just fully rebuild the tree, how computationally expensive could it even be + kt.todo_tree = TodoTree::construct(&kt.db).await.expect("Must not error"); + let tree = &kt.todo_tree; debug!("tree after refresh {tree:#?}"); @@ -162,11 +169,11 @@ impl Todo<'_> { }; let tree = &self.kh.read().await.todo_tree.tree; - *inspector = tree - .get(selected_node_id) - .expect("Nodeid must be valid") - .data() - .into(); + inspector.inspect( + tree.get(selected_node_id) + .expect("Nodeid must be valid") + .data(), + ); } } @@ -230,7 +237,7 @@ impl Component for Todo<'_> { ) .expect("Node id must be valid"); - let mut inspector: Inspector<'_> = first.data().into(); + let mut inspector: Inspector<'_> = Inspector::new(self.kh.clone(), first.data()); explorer.set_inactive(); inspector.set_inactive(); @@ -239,6 +246,15 @@ impl Component for Todo<'_> { self.task_list = Some(task_list); self.inspector = Some(inspector); + self.inspector + .as_mut() + .unwrap() + .register_signal_handler(self.signal_tx.clone().unwrap())?; + Ok(()) + } + + fn register_signal_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.signal_tx = Some(tx); Ok(()) } @@ -258,7 +274,34 @@ impl Component for Todo<'_> { .as_mut() .expect("This should have already been initialized"); + let signal_tx = self + .signal_tx + .as_mut() + .expect("Invariant broken, must exist"); + + if let Ok(Some(signal)) = inspector.update(signal.clone()).await { + signal_tx.send(signal)?; + } + match signal { + Signal::Select { nanoid } => { + let node_id = self + .kh + .read() + .await + .todo_tree + .nanoid_to_nodeid + .get(&nanoid) + .expect("Invariant broken, why would we ever ask for this") + .clone(); + + let Some(pos) = explorer.id_list.iter().position(|id| node_id == *id) else { + return Ok(None); + }; + explorer.state.select(Some(pos)); + self.update_inspector_from_selection().await; + } + Signal::SwitchTo { page: Page::Todo(region), } => { @@ -283,6 +326,7 @@ impl Component for Todo<'_> { self.update_inspector_from_selection().await; } + Signal::MoveDown => { match self.active { TodoRegion::TaskList => { @@ -326,18 +370,17 @@ impl Component for Todo<'_> { else { return Ok(None); }; - let task = Task::new( - NanoId::default().to_string(), - parent, - &mut kt, - None, - Priority::default(), - ) - .await?; + let task = + Task::new(DEFAULT_NAME, parent, &mut kt, None, Priority::default()).await?; drop(kt); debug!("created task: {task:#?}"); - return Ok(Some(Signal::Refresh)); + + signal_tx.send(Signal::Refresh)?; + signal_tx.send(Signal::Select { nanoid: task.id })?; + return Ok(Some(Signal::SwitchTo { + page: Page::Todo(TodoRegion::Inspector), + })); } Signal::NewSubGroup => { @@ -348,10 +391,15 @@ impl Component for Todo<'_> { let parent = explorer .group_of_current_selection(&kt.todo_tree) .map(|parent| parent.id.clone()); - let group = Group::new(NanoId::default().to_string(), parent, &mut kt).await?; + let group = Group::new(DEFAULT_NAME, parent, &mut kt).await?; drop(kt); debug!("Created group: {group:#?}"); - return Ok(Some(Signal::Refresh)); + + signal_tx.send(Signal::Refresh)?; + signal_tx.send(Signal::Select { nanoid: group.id })?; + return Ok(Some(Signal::SwitchTo { + page: Page::Todo(TodoRegion::Inspector), + })); } Signal::NewGroup => { @@ -360,24 +408,36 @@ impl Component for Todo<'_> { } debug!("Creating Group!"); let mut kt = self.kh.write().await; - let group = Group::new(NanoId::default().to_string(), None, &mut kt).await?; + let group = Group::new(DEFAULT_NAME, None, &mut kt).await?; drop(kt); debug!("Created group: {group:#?}"); - return Ok(Some(Signal::Refresh)); + + signal_tx.send(Signal::Refresh)?; + signal_tx.send(Signal::Select { nanoid: group.id })?; + return Ok(Some(Signal::SwitchTo { + page: Page::Todo(TodoRegion::Inspector), + })); } Signal::Refresh => { self.refresh().await; } + _ => {} } Ok(None) } + async fn handle_key_event(&mut self, key: KeyEvent) -> color_eyre::Result> { + self.inspector.as_mut().unwrap().handle_key_event(key).await + } + fn draw(&mut self, frame: &mut Frame, area: Rect) -> color_eyre::Result<()> { let explorer = self.explorer.as_mut().unwrap(); let task_list = self.task_list.as_mut().unwrap(); + let inspector = self.inspector.as_mut().unwrap(); + let splits = self.layouts.split(area); frame.render_stateful_widget(&explorer.render_list, splits.explorer, &mut explorer.state); @@ -386,7 +446,9 @@ impl Component for Todo<'_> { splits.task_list, &mut task_list.state, ); - frame.render_widget(self.inspector.as_ref().unwrap(), splits.inspector); + + inspector.draw(frame, splits.inspector)?; + Ok(()) } } diff --git a/src/tui/components/todo/tasklist.rs b/src/tui/components/todo/tasklist.rs index 53c53b5..467c0d9 100644 --- a/src/tui/components/todo/tasklist.rs +++ b/src/tui/components/todo/tasklist.rs @@ -22,7 +22,7 @@ impl TaskList<'_> { let render_list = List::new( tree.tree .traverse_pre_order(scope) - .expect("nthis should not panic as the node id should exist inside") + .expect("This should not panic as the node id should exist inside") .zip( tree.tree .traverse_pre_order_ids(scope) diff --git a/src/tui/signal.rs b/src/tui/signal.rs index 028fa1e..dc947a2 100644 --- a/src/tui/signal.rs +++ b/src/tui/signal.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use dto::NanoId; use strum::Display; use serde::{Deserialize, Serialize}; @@ -69,10 +70,26 @@ pub enum Signal { /// Only works with the inspector EditName, + /// Edit the `Priority` of a `Task` or a `Group`. + /// Only works with the inspector + EditPriority, + + /// Internal Signal that tells the app to resume interpreting keys + ExitRawText, + + /// Internal Signal that tells the app to stop interpreting keys + /// as signals + EnterRawText, + /// this is fucking temporary Helix { path: PathBuf, }, + + /// Requests the `Explorer` to select the following `NanoId`. + Select { + nanoid: NanoId, + }, } // impl FromStr for Signal { diff --git a/src/types/kasten/todo_tree.rs b/src/types/kasten/todo_tree.rs index 1bab96d..4210663 100644 --- a/src/types/kasten/todo_tree.rs +++ b/src/types/kasten/todo_tree.rs @@ -234,4 +234,22 @@ impl TodoTree { Ordering::Equal }); } + + pub fn get_node_by_nano_id(&self, nano_id: &NanoId) -> &Node { + let node_id = self + .nanoid_to_nodeid + .get(nano_id) + .expect("invariant broken!"); + + self.tree.get(node_id).expect("Invariant Broken!") + } + + pub fn get_node_mut_by_nano_id(&mut self, nano_id: &NanoId) -> &mut Node { + let node_id = self + .nanoid_to_nodeid + .get(nano_id) + .expect("invariant broken!"); + + self.tree.get_mut(node_id).expect("Invariant Broken!") + } }