aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/tui
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-03-30 15:20:31 -0700
committerGitHub <noreply@github.com>2026-03-30 15:20:31 -0700
commit0478a05320ff7bc4257633bc945bd750864d68d4 (patch)
tree2e1abf5b0a49fe0270b70bd6385062ea81fb0ba1 /crates/atuin-ai/src/tui
parentfix: replace `e>|` with `|` in nushell integration to restore history recordi... (diff)
downloadatuin-0478a05320ff7bc4257633bc945bd750864d68d4.zip
chore: Update to eye-declare 0.3.0 (#3365)
Diffstat (limited to 'crates/atuin-ai/src/tui')
-rw-r--r--crates/atuin-ai/src/tui/components/atuin_ai.rs71
-rw-r--r--crates/atuin-ai/src/tui/components/input_box.rs175
-rw-r--r--crates/atuin-ai/src/tui/components/markdown.rs16
-rw-r--r--crates/atuin-ai/src/tui/state.rs2
-rw-r--r--crates/atuin-ai/src/tui/view/mod.rs188
5 files changed, 179 insertions, 273 deletions
diff --git a/crates/atuin-ai/src/tui/components/atuin_ai.rs b/crates/atuin-ai/src/tui/components/atuin_ai.rs
index b2239a70..fab29502 100644
--- a/crates/atuin-ai/src/tui/components/atuin_ai.rs
+++ b/crates/atuin-ai/src/tui/components/atuin_ai.rs
@@ -7,7 +7,7 @@
use std::sync::mpsc;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
-use eye_declare::{Component, EventResult, Hooks, Tracked, impl_slot_children};
+use eye_declare::{Elements, EventResult, Hooks, component, props};
use crate::tui::events::AiTuiEvent;
use crate::tui::state::AppMode;
@@ -16,60 +16,31 @@ use crate::tui::state::AppMode;
///
/// Props carry the current mode so `handle_event` can translate keys
/// into the right `AiTuiEvent`. Children are rendered via slot children.
-pub struct AtuinAi {
+#[props]
+pub(crate) struct AtuinAi {
pub mode: AppMode,
pub has_command: bool,
pub is_input_blank: bool,
pub pending_confirmation: bool,
}
-impl Default for AtuinAi {
- fn default() -> Self {
- Self {
- mode: AppMode::Input,
- has_command: false,
- is_input_blank: false,
- pending_confirmation: false,
- }
- }
-}
-
-impl_slot_children!(AtuinAi);
-
#[derive(Default)]
pub struct AtuinAiState {
tx: Option<mpsc::Sender<AiTuiEvent>>,
}
-impl Component for AtuinAi {
- type State = AtuinAiState;
-
- fn initial_state(&self) -> Option<Self::State> {
- Some(AtuinAiState::default())
- }
-
- fn lifecycle(&self, hooks: &mut Hooks<Self::State>, _state: &Self::State) {
- hooks.use_context::<mpsc::Sender<AiTuiEvent>>(|tx, state| {
- state.tx = tx.cloned();
- });
- }
-
- fn render(
- &self,
- _area: ratatui::layout::Rect,
- _buf: &mut ratatui::buffer::Buffer,
- _state: &Self::State,
- ) {
- // Rendering is handled by slot children
- }
-
- fn desired_height(&self, _width: u16, _state: &Self::State) -> u16 {
- 0
- }
-
- fn handle_event_capture(&self, event: &Event, state: &mut Tracked<Self::State>) -> EventResult {
- let state = state.read();
+#[component(props = AtuinAi, state = AtuinAiState, children = Elements)]
+fn atuin_ai(
+ _props: &AtuinAi,
+ _state: &AtuinAiState,
+ hooks: &mut Hooks<AtuinAi, AtuinAiState>,
+ children: Elements,
+) -> Elements {
+ hooks.use_context::<mpsc::Sender<AiTuiEvent>>(|tx, _, state| {
+ state.tx = tx.cloned();
+ });
+ hooks.use_event_capture(move |event, props, state| {
let Event::Key(KeyEvent {
code,
kind: KeyEventKind::Press,
@@ -80,7 +51,7 @@ impl Component for AtuinAi {
return EventResult::Ignored;
};
- let Some(ref tx) = state.tx else {
+ let Some(ref tx) = state.read().tx else {
return EventResult::Ignored;
};
@@ -90,10 +61,10 @@ impl Component for AtuinAi {
return EventResult::Consumed;
}
- match self.mode {
+ match props.mode {
AppMode::Input => match code {
KeyCode::Esc => {
- if self.pending_confirmation {
+ if props.pending_confirmation {
let _ = tx.send(AiTuiEvent::CancelConfirmation);
return EventResult::Consumed;
}
@@ -102,7 +73,7 @@ impl Component for AtuinAi {
EventResult::Consumed
}
KeyCode::Tab => {
- if self.has_command && self.is_input_blank {
+ if props.has_command && props.is_input_blank {
let _ = tx.send(AiTuiEvent::InsertCommand);
return EventResult::Consumed;
}
@@ -110,7 +81,7 @@ impl Component for AtuinAi {
EventResult::Ignored
}
KeyCode::Enter => {
- if self.has_command && self.is_input_blank {
+ if props.has_command && props.is_input_blank {
let _ = tx.send(AiTuiEvent::ExecuteCommand);
return EventResult::Consumed;
}
@@ -138,5 +109,7 @@ impl Component for AtuinAi {
_ => EventResult::Ignored,
},
}
- }
+ });
+
+ children
}
diff --git a/crates/atuin-ai/src/tui/components/input_box.rs b/crates/atuin-ai/src/tui/components/input_box.rs
index 3167ecc1..f5e0fe2b 100644
--- a/crates/atuin-ai/src/tui/components/input_box.rs
+++ b/crates/atuin-ai/src/tui/components/input_box.rs
@@ -6,13 +6,12 @@
//!
//! On Enter, sends `AiTuiEvent::SubmitInput` via the context-provided channel.
-use std::sync::{Mutex, mpsc};
+use std::sync::{Arc, Mutex, mpsc};
use crossterm::event::KeyModifiers;
-use eye_declare::{Component, EventResult, Hooks, Tracked};
+use eye_declare::{Canvas, Elements, EventResult, Hooks, component, element, props};
use ratatui::widgets::{Block, Borders, Padding};
use ratatui_core::{
- buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::Line,
@@ -26,8 +25,8 @@ use crate::tui::events::AiTuiEvent;
///
/// Props configure the chrome (title, footer). The TextArea itself lives
/// in the component's State so it owns cursor, wrapping, and rendering.
-#[derive(Default)]
-pub struct InputBox {
+#[props]
+pub(crate) struct InputBox {
/// Title shown in top-left border
pub title: String,
/// Right-side label in top border
@@ -38,8 +37,8 @@ pub struct InputBox {
pub active: bool,
}
-pub struct InputBoxState {
- textarea: Mutex<TextArea<'static>>,
+pub(crate) struct InputBoxState {
+ textarea: Arc<Mutex<TextArea<'static>>>,
tx: Option<mpsc::Sender<AiTuiEvent>>,
}
@@ -55,109 +54,58 @@ impl Default for InputBoxState {
.add_modifier(ratatui::style::Modifier::ITALIC),
);
Self {
- textarea: Mutex::new(textarea),
+ textarea: Arc::new(Mutex::new(textarea)),
tx: None,
}
}
}
-impl InputBox {
- /// Build the ratatui Block with current titles/footer.
- fn make_block(&self) -> Block<'_> {
- let border_style = Style::default().fg(Color::DarkGray);
- let title_style = Style::default()
- .fg(Color::Gray)
- .add_modifier(Modifier::BOLD);
+fn make_block(props: &InputBox) -> Block<'static> {
+ let border_style = Style::default().fg(Color::DarkGray);
+ let title_style = Style::default()
+ .fg(Color::Gray)
+ .add_modifier(Modifier::BOLD);
- let mut block = Block::default()
- .borders(Borders::ALL)
- .border_style(border_style)
- .padding(Padding::horizontal(1));
+ let mut block = Block::default()
+ .borders(Borders::ALL)
+ .border_style(border_style)
+ .padding(Padding::horizontal(1));
- if !self.title.is_empty() {
- block = block
- .title_top(Line::styled(format!(" {} ", self.title), title_style).left_aligned());
- }
- if !self.title_right.is_empty() {
- block = block.title_top(
- Line::styled(format!(" {} ", self.title_right), border_style).right_aligned(),
- );
- }
- if !self.footer.is_empty() {
- block = block.title_bottom(
- Line::styled(format!(" {} ", self.footer), border_style).right_aligned(),
- );
- }
-
- block
+ if !props.title.is_empty() {
+ block =
+ block.title_top(Line::styled(format!(" {} ", props.title), title_style).left_aligned());
}
-}
-
-impl Component for InputBox {
- type State = InputBoxState;
-
- fn initial_state(&self) -> Option<InputBoxState> {
- Some(InputBoxState::default())
+ if !props.title_right.is_empty() {
+ block = block.title_top(
+ Line::styled(format!(" {} ", props.title_right), border_style).right_aligned(),
+ );
}
-
- fn lifecycle(&self, hooks: &mut Hooks<Self::State>, _state: &Self::State) {
- if self.active {
- hooks.use_autofocus();
- }
- hooks.use_context::<mpsc::Sender<AiTuiEvent>>(|tx, state| {
- state.tx = tx.cloned();
- });
+ if !props.footer.is_empty() {
+ block = block.title_bottom(
+ Line::styled(format!(" {} ", props.footer), border_style).right_aligned(),
+ );
}
- fn render(&self, area: Rect, buf: &mut Buffer, state: &Self::State) {
- if area.height < 3 || area.width < 4 {
- return;
- }
- // Configure the block on each render so titles/footer stay current.
- // Note: set_block takes ownership, but the block is cheap to rebuild.
- // We can't call set_block here since we only have &self/&state,
- // so we render block + textarea separately.
- let block = self.make_block();
- let inner = block.inner(area);
- block.render(area, buf);
-
- let mut textarea = state.textarea.lock().unwrap();
- if self.active {
- textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
- textarea.set_placeholder_text("Type a message...");
- } else {
- textarea.set_cursor_style(Style::default());
- textarea.set_placeholder_text("");
- }
-
- // Render textarea into the inner area
- textarea.render(inner, buf);
- }
+ block
+}
- fn desired_height(&self, width: u16, state: &Self::State) -> u16 {
- if width < 4 {
- return 3;
- }
- // TextArea handles scrolling internally if content overflows.
- let block = self.make_block();
- let inner = block.inner(Rect::new(0, 0, width, u16::MAX));
- let chrome = (u16::MAX).saturating_sub(inner.height);
- let content = state.textarea.lock().unwrap().measure(width - 4);
- chrome + content.preferred_rows
- }
+#[component(props = InputBox, state = InputBoxState)]
+fn input_box(
+ props: &InputBox,
+ state: &InputBoxState,
+ hooks: &mut Hooks<InputBox, InputBoxState>,
+) -> Elements {
+ hooks.use_focusable(props.active);
+ hooks.use_autofocus();
- fn is_focusable(&self, _state: &Self::State) -> bool {
- self.active
- }
+ hooks.use_context::<mpsc::Sender<AiTuiEvent>>(|tx, _, state| {
+ state.tx = tx.cloned();
+ });
- fn handle_event(
- &self,
- event: &crossterm::event::Event,
- state: &mut Tracked<Self::State>,
- ) -> EventResult {
+ hooks.use_event(move |event, props, state| {
let state = state.read();
- if !self.active {
+ if !props.active {
return EventResult::Ignored;
}
@@ -213,5 +161,42 @@ impl Component for InputBox {
}
EventResult::Ignored
- }
+ });
+
+ let textarea = state.textarea.clone();
+ let block = make_block(props);
+ let active = props.active;
+ element!(
+ Canvas(render_fn: move |area, buf| {
+ let mut area = area;
+
+ if area.height < 3 || area.width < 4 {
+ return;
+ }
+
+ let height = {
+ // TextArea handles scrolling internally if content overflows.
+ let inner = block.inner(Rect::new(0, 0, area.width, u16::MAX));
+ let chrome = (u16::MAX).saturating_sub(inner.height);
+ let content = textarea.lock().unwrap().measure(area.width - 4);
+ chrome + content.preferred_rows
+ };
+
+ area.height = height.min(7);
+ let inner = block.clone().inner(area);
+ block.clone().render(area, buf);
+
+ let mut textarea = textarea.lock().unwrap();
+ if active {
+ textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
+ textarea.set_placeholder_text("Type a message...");
+ } else {
+ textarea.set_cursor_style(Style::default());
+ textarea.set_placeholder_text("");
+ }
+
+ // Render textarea into the inner area
+ textarea.render(inner, buf);
+ })
+ )
}
diff --git a/crates/atuin-ai/src/tui/components/markdown.rs b/crates/atuin-ai/src/tui/components/markdown.rs
index e1551a7f..1cd7dbcf 100644
--- a/crates/atuin-ai/src/tui/components/markdown.rs
+++ b/crates/atuin-ai/src/tui/components/markdown.rs
@@ -3,7 +3,7 @@
//! More robust than eye-declare's built-in Markdown component:
//! uses a proper CommonMark parser rather than line-by-line regex.
-use eye_declare::Component;
+use eye_declare::{Component, props};
use pulldown_cmark::{Event, Parser, Tag, TagEnd};
use ratatui_core::{
buffer::Buffer,
@@ -15,7 +15,7 @@ use ratatui_core::{
use ratatui_widgets::paragraph::{Paragraph, Wrap};
/// A markdown rendering component backed by pulldown-cmark.
-#[derive(Default)]
+#[props]
pub struct Markdown {
pub source: String,
}
@@ -73,14 +73,16 @@ impl Component for Markdown {
.render(area, buf);
}
- fn desired_height(&self, width: u16, state: &Self::State) -> u16 {
+ fn desired_height(&self, width: u16, state: &Self::State) -> Option<u16> {
if self.source.is_empty() || width == 0 {
- return 0;
+ return Some(0);
}
let text = parse_markdown(&self.source, state);
- Paragraph::new(text)
- .wrap(Wrap { trim: false })
- .line_count(width) as u16
+ Some(
+ Paragraph::new(text)
+ .wrap(Wrap { trim: false })
+ .line_count(width) as u16,
+ )
}
fn initial_state(&self) -> Option<MarkdownStyles> {
diff --git a/crates/atuin-ai/src/tui/state.rs b/crates/atuin-ai/src/tui/state.rs
index c7271d29..4c5c2a1e 100644
--- a/crates/atuin-ai/src/tui/state.rs
+++ b/crates/atuin-ai/src/tui/state.rs
@@ -113,7 +113,7 @@ impl ConversationEvent {
}
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum AppMode {
/// User is typing input
Input,
diff --git a/crates/atuin-ai/src/tui/view/mod.rs b/crates/atuin-ai/src/tui/view/mod.rs
index a1b32518..0cd51dfa 100644
--- a/crates/atuin-ai/src/tui/view/mod.rs
+++ b/crates/atuin-ai/src/tui/view/mod.rs
@@ -1,8 +1,7 @@
//! View function that builds the eye-declare element tree from app state.
use eye_declare::{
- Column, Component, Elements, HStack, Line, Span, Spinner, TextBlock, VStack, WidthConstraint,
- element, impl_slot_children,
+ Cells, Column, Elements, HStack, Span, Spinner, Text, View, WidthConstraint, element,
};
use ratatui_core::style::{Color, Modifier, Style};
@@ -13,40 +12,6 @@ use super::state::{AppMode, AppState};
mod turn;
-#[derive(Default)]
-struct Padding {
- top: u16,
- left: u16,
- right: u16,
- bottom: u16,
-}
-
-impl Component for Padding {
- type State = ();
-
- fn content_inset(&self, _state: &Self::State) -> eye_declare::Insets {
- eye_declare::Insets::ZERO
- .left(self.left)
- .right(self.right)
- .top(self.top)
- .bottom(self.bottom)
- }
-
- fn desired_height(&self, _width: u16, _state: &Self::State) -> u16 {
- 0
- }
-
- fn render(
- &self,
- _area: ratatui::layout::Rect,
- _buf: &mut ratatui::buffer::Buffer,
- _state: &(),
- ) {
- }
-}
-
-impl_slot_children!(Padding);
-
/// Build the element tree from current state.
///
/// Layout (top to bottom):
@@ -68,7 +33,7 @@ pub fn ai_view(state: &AppState) -> Elements {
element! {
AtuinAi(
- mode: state.mode.clone(),
+ mode: state.mode,
has_command: state.has_any_command(),
is_input_blank: state.is_input_blank,
pending_confirmation: state.confirmation_pending,
@@ -88,22 +53,24 @@ pub fn ai_view(state: &AppState) -> Elements {
})
#(if !state.is_exiting() {
- TextBlock { Line { Span(text: "") } }
- InputBox(
- key: "input",
- title: "Generate a command or ask a question",
- title_right: "Atuin AI",
- footer: state.footer_text(),
- active: state.mode == AppMode::Input && !state.confirmation_pending,
- )
+ View(key: "input-box", padding_top: Cells::from(1)) {
+ InputBox(
+ key: "input",
+ title: "Generate a command or ask a question",
+ title_right: "Atuin AI",
+ footer: state.footer_text(),
+ active: state.mode == AppMode::Input && !state.confirmation_pending,
+ )
- #(if state.is_input_blank && state.has_any_command() && state.mode == AppMode::Input {
- #(if state.confirmation_pending {
- TextBlock { Line { Span(text: "[Enter] Confirm dangerous command [Esc] Cancel", style: Style::default().fg(Color::Gray)) } }
- } else {
- TextBlock { Line { Span(text: "[Enter] Execute suggested command [Tab] Insert Command", style: Style::default().fg(Color::Gray)) } }
+ #(if state.is_input_blank && state.has_any_command() && state.mode == AppMode::Input {
+ #(if state.confirmation_pending {
+ Text { Span(text: "[Enter] Confirm dangerous command [Esc] Cancel", style: Style::default().fg(Color::Gray)) }
+ } else {
+ Text { Span(text: "[Enter] Execute suggested command [Tab] Insert Command", style: Style::default().fg(Color::Gray)) }
+ })
})
- })
+
+ }
})
}
}
@@ -114,25 +81,20 @@ fn user_turn_view(events: &[turn::UiEvent], first_turn: bool) -> Elements {
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD);
+ let padding = if first_turn { 0 } else { 1 };
+
element! {
- VStack {
- TextBlock {
- #(if !first_turn {
- Line { Span() }
- })
- Line {
- Span(text: "You", style: label_style)
- }
+ View(padding_top: Cells::from(padding)) {
+ Text {
+ Span(text: "You", style: label_style)
}
#(for event in events {
#(match event {
turn::UiEvent::Text { content } => {
element! {
- Padding(left: 2u16) {
- TextBlock {
- Line {
- Span(text: content, style: Style::default())
- }
+ View(padding_left: Cells::from(2)) {
+ Text {
+ Span(text: content, style: Style::default())
}
}
}
@@ -150,7 +112,7 @@ fn agent_turn_view(events: &[turn::UiEvent], busy: bool) -> Elements {
.add_modifier(Modifier::BOLD);
element! {
- VStack {
+ View {
Spinner(
label: "Atuin AI",
label_style: label_style,
@@ -163,7 +125,7 @@ fn agent_turn_view(events: &[turn::UiEvent], busy: bool) -> Elements {
#(match event {
turn::UiEvent::Text { content } => {
element! {
- Padding(left: 2u16) {
+ View(padding_left: Cells::from(2)) {
Markdown(source: content)
}
}
@@ -183,9 +145,9 @@ fn agent_turn_view(events: &[turn::UiEvent], busy: bool) -> Elements {
fn out_of_band_turn_view(events: &[turn::UiEvent]) -> Elements {
element! {
- VStack {
- TextBlock {
- Line { Span(text: "System", style: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)) }
+ View {
+ Text {
+ Span(text: "System", style: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD))
}
#(for event in events {
#(match event {
@@ -201,12 +163,10 @@ fn out_of_band_turn_view(events: &[turn::UiEvent]) -> Elements {
fn out_of_band_output_view(details: &turn::OutOfBandOutputDetails) -> Elements {
element! {
- Padding(left: 2u16) {
+ View(padding_left: Cells::from(2)) {
#(if details.command.is_some() {
- TextBlock {
- Line {
- Span(text: details.command.as_ref().unwrap(), style: Style::default().fg(Color::Blue))
- }
+ Text {
+ Span(text: details.command.as_ref().unwrap(), style: Style::default().fg(Color::Blue))
}
})
Markdown(source: details.content.clone())
@@ -254,82 +214,68 @@ fn suggested_command_view(details: &turn::SuggestedCommandDetails) -> Elements {
let confidence_notes = details.confidence_level.notes();
element! {
- VStack {
- TextBlock {
- #(if !details.first_event_in_turn {
- Line { Span() }
- })
- Line {
- Span(text: " Suggested command:", style: Style::default().fg(Color::Cyan))
- }
+ View {
+ #(if !details.first_event_in_turn {
+ Text { Span(text: "") }
+ })
+ Text {
+ Span(text: " Suggested command:", style: Style::default().fg(Color::Cyan))
}
HStack {
- Column(width: WidthConstraint::Fixed(2)) {
- TextBlock {
- Line {
- #(if is_dangerous || low_confidence {
- Span(text: "! ", style: Style::default().fg(Color::Yellow))
- } else {
- Span(text: "$ ", style: Style::default().fg(Color::Blue))
- })
- }
+ View(width: WidthConstraint::Fixed(2)) {
+ Text {
+ #(if is_dangerous || low_confidence {
+ Span(text: "! ", style: Style::default().fg(Color::Yellow))
+ } else {
+ Span(text: "$ ", style: Style::default().fg(Color::Blue))
+ })
}
}
Column {
- TextBlock {
- Line {
- Span(text: &details.command, style: Style::default().fg(Color::Green))
- }
+ Text {
+ Span(text: &details.command, style: Style::default().fg(Color::Green))
}
}
}
#(if is_dangerous {
- Padding(left: 2u16) {
- TextBlock {
- Line {
- Span(text: "Danger: ", style: danger_style)
- Span(text: danger_text, style: danger_style.add_modifier(Modifier::BOLD))
- }
+ View(padding_left: Cells::from(2)) {
+ Text {
+ Span(text: "Danger: ", style: danger_style)
+ Span(text: danger_text, style: danger_style.add_modifier(Modifier::BOLD))
}
}
})
#(if is_dangerous && danger_notes.is_some() {
- Padding(left: 2u16) {
+ View(padding_left: Cells::from(2)) {
HStack {
- Column(width: WidthConstraint::Fixed(2)) {
- TextBlock {
- Line {
- Span(text: "└")
- }
+ View(width: WidthConstraint::Fixed(2)) {
+ Text {
+ Span(text: "└")
}
}
- Column(width: WidthConstraint::Fill) {
+ View(width: WidthConstraint::Fill) {
Markdown(source: danger_notes.unwrap())
}
}
}
})
#(if low_confidence {
- Padding(left: 2u16) {
- TextBlock {
- Line {
- Span(text: "Confidence: ", style: Style::default().fg(Color::Blue))
- Span(text: confidence_level, style: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD))
- }
+ View(padding_left: Cells::from(2)) {
+ Text {
+ Span(text: "Confidence: ", style: Style::default().fg(Color::Blue))
+ Span(text: confidence_level, style: Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD))
}
}
})
#(if low_confidence && confidence_notes.is_some() {
- Padding(left: 2u16) {
+ View(padding_left: Cells::from(2)) {
HStack {
- Column(width: WidthConstraint::Fixed(2)) {
- TextBlock {
- Line {
- Span(text: "└")
- }
+ View(width: WidthConstraint::Fixed(2)) {
+ Text {
+ Span(text: "└")
}
}
- Column(width: WidthConstraint::Fill) {
+ View(width: WidthConstraint::Fill) {
Markdown(source: confidence_notes.unwrap())
}
}