diff --git a/lumni/src/apps/api/error.rs b/lumni/src/apps/api/error.rs index 1038770..9e1d150 100644 --- a/lumni/src/apps/api/error.rs +++ b/lumni/src/apps/api/error.rs @@ -14,11 +14,17 @@ pub enum LumniError { Runtime(RuntimeError), Application(ApplicationError, Option), Invoke(ApplicationError, Option), + Resource(ResourceError), NotImplemented(String), Message(String), Any(String), } +#[derive(Debug, Clone)] +pub enum ResourceError { + NotFound(String), +} + #[derive(Debug, Clone)] pub enum RequestError { QueryInvalid(String), @@ -65,6 +71,9 @@ impl fmt::Display for LumniError { LumniError::Invoke(app_err, Some(app_name)) => { write!(f, "[{}]: {}", app_name, app_err) } + LumniError::Resource(res_err) => { + write!(f, "ResourceError: {:?}", res_err) + } LumniError::Invoke(app_err, None) => { write!(f, "InvokeError: {}", app_err) } diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs index 3eecd35..1fabce8 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/session/conversation_loop.rs @@ -205,7 +205,9 @@ async fn handle_window_event( app.get_conversation_id_for_active_session(), ) .await?; - Ok(window_event) + // refresh at least once after opening modal + app.is_processing = true; + Ok(WindowEvent::Modal(ModalAction::Refresh)) } ModalAction::Event(ref user_event) => { handle_modal_user_event(app, user_event, db_conn).await?; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs index aa8d946..64cf130 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs @@ -7,7 +7,7 @@ use lumni::api::error::ApplicationError; use super::key_event::KeyTrack; use super::text_window_event::handle_text_window_event; use super::{ - AppUi, LineType, PromptAction, PromptWindow, TextWindowTrait, WindowEvent, + AppUi, LineType, PromptAction, TextArea, TextWindowTrait, WindowEvent, }; use crate::apps::builtin::llm::prompt::src::tui::WindowKind; pub use crate::external as lumni; @@ -88,7 +88,7 @@ pub fn handle_prompt_window_event( return Ok(app_ui.set_prompt_window(true)); } '+' => { - app_ui.set_primary_window(WindowKind::PromptWindow); + app_ui.set_primary_window(WindowKind::EditorWindow); } '-' => { app_ui.set_primary_window(WindowKind::ResponseWindow); @@ -110,7 +110,7 @@ pub fn handle_prompt_window_event( handle_text_window_event(key_track, &mut app_ui.prompt, is_running) } -fn is_closed_block(prompt_window: &mut PromptWindow) -> Option { +fn is_closed_block(prompt_window: &mut TextArea) -> Option { // return None if not inside a block // return Some(true) if block is closed, else return Some(false) let code_block = prompt_window.current_code_block(); @@ -121,7 +121,7 @@ fn is_closed_block(prompt_window: &mut PromptWindow) -> Option { } fn ensure_closed_block( - prompt_window: &mut PromptWindow, + prompt_window: &mut TextArea, ) -> Result<(), ApplicationError> { if let Some(closed_block) = is_closed_block(prompt_window) { if !closed_block { @@ -132,7 +132,7 @@ fn ensure_closed_block( Ok(()) } -fn in_editing_block(prompt_window: &mut PromptWindow) -> bool { +fn in_editing_block(prompt_window: &mut TextArea) -> bool { let line_type = prompt_window.current_line_type().unwrap_or(LineType::Text); match line_type { LineType::Code(block_line) => !block_line.is_end(), diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs index 0aded46..e426a48 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_response_window.rs @@ -54,7 +54,7 @@ pub fn handle_response_window_event( app_ui.set_primary_window(WindowKind::ResponseWindow); } '-' => { - app_ui.set_primary_window(WindowKind::PromptWindow); + app_ui.set_primary_window(WindowKind::EditorWindow); } ' ' => { if let Some(prev) = key_track.previous_key_str() { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs index 6e7285e..9fbb106 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs @@ -12,7 +12,7 @@ use super::clipboard::ClipboardProvider; use super::modals::{ModalAction, ModalWindowType}; use super::ui::AppUi; use super::window::{ - LineType, MoveCursor, PromptWindow, TextDocumentTrait, TextWindowTrait, + LineType, MoveCursor, TextArea, TextDocumentTrait, TextWindowTrait, WindowKind, }; use super::{ConversationDbHandler, NewConversation, ThreadedChatSession}; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs index def4e39..0e0e837 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/text_window_event.rs @@ -100,7 +100,7 @@ where let kind = match window.get_kind() { WindowKind::ResponseWindow => WindowEvent::ResponseWindow, - WindowKind::PromptWindow => WindowEvent::PromptWindow(None), + WindowKind::EditorWindow => WindowEvent::PromptWindow(None), WindowKind::CommandLine => WindowEvent::CommandLine(None), }; Ok(kind) @@ -207,7 +207,7 @@ where } let kind = match window.get_kind() { WindowKind::ResponseWindow => WindowEvent::ResponseWindow, - WindowKind::PromptWindow => WindowEvent::PromptWindow(None), + WindowKind::EditorWindow => WindowEvent::PromptWindow(None), WindowKind::CommandLine => WindowEvent::CommandLine(None), }; Ok(kind) diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs index 9ef6c4d..f5b5caf 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs @@ -16,13 +16,13 @@ use lumni::api::error::ApplicationError; pub use modals::{ModalAction, ModalWindowTrait, ModalWindowType}; pub use ui::AppUi; pub use window::{ - CommandLine, PromptWindow, ResponseWindow, SimpleString, TextLine, - TextSegment, TextWindowTrait, WindowKind, + CommandLine, ResponseWindow, SimpleString, TextArea, TextLine, TextSegment, + TextWindowTrait, WindowKind, }; use super::chat::db::{ Conversation, ConversationDatabase, ConversationDbHandler, ConversationId, - ConversationStatus, MaskMode, ModelSpec, UserProfile, UserProfileDbHandler, + ConversationStatus, MaskMode, UserProfile, UserProfileDbHandler, }; use super::chat::{ App, NewConversation, PromptInstruction, ThreadedChatSession, diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs index 4bff1e0..f3396fb 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs @@ -16,9 +16,9 @@ use ratatui::widgets::{ use ratatui::Frame; use super::{ - ApplicationError, CommandLine, Conversation, ConversationDbHandler, - ConversationStatus, KeyTrack, ModalAction, ModalWindowTrait, - ModalWindowType, PromptInstruction, TextWindowTrait, ThreadedChatSession, + ApplicationError, Conversation, ConversationDbHandler, ConversationStatus, + KeyTrack, ModalAction, ModalWindowTrait, ModalWindowType, + PromptInstruction, TextArea, TextWindowTrait, ThreadedChatSession, UserEvent, WindowEvent, }; use crate::apps::builtin::llm::prompt::src::chat::db::ConversationId; @@ -31,7 +31,7 @@ pub struct ConversationListModal<'a> { conversations: Vec, current_tab: ConversationStatus, tab_indices: HashMap, - edit_name_line: Option>, + edit_name_line: Option>, editing_index: Option, last_selected_conversation_id: Option, } @@ -234,7 +234,7 @@ impl<'a> ConversationListModal<'a> { async fn edit_conversation_name(&mut self) -> Result<(), ApplicationError> { if let Some(conversation) = self.get_current_conversation() { - let mut command_line = CommandLine::new(); + let mut command_line = TextArea::new(); command_line.text_set(&conversation.name, None)?; command_line.set_status_insert(); self.edit_name_line = Some(command_line); diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs index 9cb8f44..f7b0738 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs @@ -19,7 +19,8 @@ use tokio::sync::mpsc; use super::{ ApplicationError, ConversationDbHandler, KeyTrack, ModalAction, - ModalWindowTrait, ModalWindowType, ThreadedChatSession, WindowEvent, + ModalWindowTrait, ModalWindowType, TextArea, TextWindowTrait, + ThreadedChatSession, WindowEvent, }; pub use crate::external as lumni; @@ -51,11 +52,18 @@ impl FileListHandler { ) -> Result>, ApplicationError> { let query = format!("SELECT * FROM \"localfs://{}\" LIMIT 100", path); let config = EnvironmentConfig::new(HashMap::new()); - self.handler + match self + .handler .execute_query(&query, &config, true, false, None, None) .await - .map(|table| Arc::new(table as Box)) - .map_err(|e| ApplicationError::InternalError(e.to_string())) + { + Ok(table) => Ok(Arc::new(table as Box)), + Err(e) => match e { + // TODO: update execute_query to return LumniError::ResourceError + //ResourceError::NotFound => Err(ApplicationError::NotFound(path)), + _ => Err(ApplicationError::InternalError(e.to_string())), + }, + } } } @@ -64,8 +72,9 @@ pub enum BackgroundTaskResult { DirectoryChange(Result), } -pub struct FileBrowserModal { +pub struct FileBrowserModal<'a> { current_path: String, + path_input: TextArea<'a>, file_table: Option>>, selected_index: usize, scroll_offset: usize, @@ -74,9 +83,17 @@ pub struct FileBrowserModal { operation_sender: mpsc::Sender, task_start_time: Option, list_displayable: bool, + selected_file_content: Option, + focus: FileBrowserFocus, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum FileBrowserFocus { + PathInput, + FileList, } -impl FileBrowserModal { +impl<'a> FileBrowserModal<'a> { pub fn new(initial_path: String) -> Self { let (op_tx, op_rx) = mpsc::channel(100); let (result_tx, result_rx) = mpsc::channel(100); @@ -90,8 +107,12 @@ impl FileBrowserModal { }); }); + let mut path_input = TextArea::new(); + path_input.text_set(&initial_path, None).unwrap(); + let mut modal = Self { current_path: initial_path, + path_input, file_table: None, selected_index: 0, scroll_offset: 0, @@ -100,6 +121,8 @@ impl FileBrowserModal { operation_sender: op_tx, task_start_time: None, list_displayable: true, + selected_file_content: None, + focus: FileBrowserFocus::FileList, }; modal.start_list_files(); @@ -425,11 +448,30 @@ impl FileBrowserModal { } } - fn render_current_path(&self, frame: &mut Frame, area: Rect) { - let path = Paragraph::new(Span::raw(&self.current_path)).block( - Block::default().title("Current Path").borders(Borders::ALL), + fn render_current_path(&mut self, frame: &mut Frame, area: Rect) { + let (path_style, border_style) = match self.focus { + FileBrowserFocus::PathInput => ( + Style::default().fg(Color::Yellow), + Style::default().fg(Color::Yellow), + ), + FileBrowserFocus::FileList => (Style::default(), Style::default()), + }; + + let path_widget = self.path_input.widget(&area).style(path_style); + + let block = Block::default() + .borders(Borders::ALL) + .border_style(border_style) + .title("Current Path"); + + frame.render_widget(block, area); + frame.render_widget( + path_widget, + area.inner(Margin { + vertical: 1, + horizontal: 1, + }), ); - frame.render_widget(path, area); } fn render_file_details(&self, frame: &mut Frame, area: Rect) { @@ -499,10 +541,42 @@ impl FileBrowserModal { frame.render_widget(loading, area); } } + + fn render_file_content(&self, frame: &mut Frame, area: Rect) { + let content = match &self.selected_file_content { + Some(content) => content, + None => "No file selected", + }; + + let paragraph = Paragraph::new(content).block( + Block::default().title("File Content").borders(Borders::ALL), + ); + + frame.render_widget(paragraph, area); + } + + fn handle_enter(&mut self) { + if let Some(table) = &self.file_table { + if let Some(row) = table.get_row(self.selected_index) { + if let Some(TableColumnValue::StringColumn(name)) = + row.get_value("name") + { + let is_dir = name.ends_with('/'); + if is_dir { + self.start_enter_directory(); + } else { + // Set placeholder content for the selected file + self.selected_file_content = + Some("Contents of the file".to_string()); + } + } + } + } + } } #[async_trait] -impl ModalWindowTrait for FileBrowserModal { +impl ModalWindowTrait for FileBrowserModal<'_> { fn get_type(&self) -> ModalWindowType { ModalWindowType::FileBrowser } @@ -513,14 +587,13 @@ impl ModalWindowTrait for FileBrowserModal { .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // Current path - Constraint::Min(1), // File list or message + Constraint::Min(1), // Main content area Constraint::Length(1), // Instructions ]) .split(area); self.render_current_path(frame, chunks[0]); - // Check if there's enough space to render the file list if chunks[1].height < 3 { self.list_displayable = false; let message = @@ -531,21 +604,26 @@ impl ModalWindowTrait for FileBrowserModal { } else { self.list_displayable = true; - // Split the main area into file list and file details - let file_areas = Layout::default() - .direction(Direction::Vertical) + let main_areas = Layout::default() + .direction(Direction::Horizontal) .constraints([ - Constraint::Min(3), // File list (minimum 3 rows) - Constraint::Length(5), // File details (5 rows) + Constraint::Percentage(50), // Files list + Constraint::Percentage(50), // File details and content ]) .split(chunks[1]); - self.render_file_list(frame, file_areas[0]); + self.render_file_list(frame, main_areas[0]); - // Only render file details if there's enough space - if chunks[1].height >= 8 { - self.render_file_details(frame, file_areas[1]); - } + let details_areas = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(5), // File meta details (5 rows) + Constraint::Min(1), // File content area + ]) + .split(main_areas[1]); + + self.render_file_details(frame, details_areas[0]); + self.render_file_content(frame, details_areas[1]); } self.render_instructions(frame, chunks[2]); @@ -561,34 +639,41 @@ impl ModalWindowTrait for FileBrowserModal { _tab_chat: &'b mut ThreadedChatSession, _handler: &mut ConversationDbHandler, ) -> Result { - if !self.list_displayable { - // If the list isn't displayable, only allow exiting - match key_event.current_key().code { + match self.focus { + FileBrowserFocus::PathInput => match key_event.current_key().code { + KeyCode::Enter => { + self.current_path = + self.path_input.text_buffer().to_string(); + self.start_list_files(); + self.focus = FileBrowserFocus::FileList; + } + KeyCode::Esc => { + self.path_input.text_set(&self.current_path, None)?; + self.focus = FileBrowserFocus::FileList; + } + KeyCode::Tab => { + self.focus = FileBrowserFocus::FileList; + } + _ => { + self.path_input.process_edit_input(key_event)?; + } + }, + FileBrowserFocus::FileList => match key_event.current_key().code { + KeyCode::Up => self.move_selection_up(), + KeyCode::Down => self.move_selection_down(), + KeyCode::Enter => self.handle_enter(), + KeyCode::PageUp | KeyCode::Char('k') => self.page_up(), + KeyCode::PageDown | KeyCode::Char('j') => self.page_down(), + KeyCode::Backspace => self.start_go_up_directory(), + KeyCode::Tab => { + self.focus = FileBrowserFocus::PathInput; + self.path_input.set_status_insert(); + } KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { return Ok(WindowEvent::PromptWindow(None)) } - _ => return Ok(WindowEvent::Modal(ModalAction::Refresh)), - } - } - - match key_event.current_key().code { - KeyCode::Up => self.move_selection_up(), - KeyCode::Down => self.move_selection_down(), - KeyCode::Enter => { - self.start_enter_directory(); - } - KeyCode::PageUp | KeyCode::Char('k') => self.page_up(), - KeyCode::PageDown | KeyCode::Char('j') => self.page_down(), - KeyCode::Backspace => { - self.start_go_up_directory(); - } - KeyCode::Char('s') | KeyCode::Char('S') => { - // TODO: search files - } - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { - return Ok(WindowEvent::PromptWindow(None)) - } - _ => {} + _ => {} + }, } Ok(WindowEvent::Modal(ModalAction::Refresh)) } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs index 823d134..ba66222 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs @@ -10,9 +10,9 @@ use ratatui::layout::Rect; use ratatui::Frame; use super::{ - ApplicationError, CommandLine, Conversation, ConversationDbHandler, - ConversationStatus, KeyTrack, MaskMode, ModelServer, PromptInstruction, - ServerTrait, SimpleString, TextWindowTrait, ThreadedChatSession, UserEvent, + ApplicationError, Conversation, ConversationDbHandler, ConversationStatus, + KeyTrack, MaskMode, ModelServer, PromptInstruction, ServerTrait, + SimpleString, TextArea, TextWindowTrait, ThreadedChatSession, UserEvent, UserProfile, UserProfileDbHandler, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, }; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs index 62a199c..57eff5c 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs @@ -1,19 +1,20 @@ use std::sync::Arc; use lumni::api::error::ApplicationError; +use ratatui::widgets::Borders; use super::modals::{ ConversationListModal, FileBrowserModal, ProfileEditModal, }; use super::{ CommandLine, ConversationDatabase, ConversationId, ModalWindowTrait, - ModalWindowType, PromptWindow, ResponseWindow, TextLine, TextWindowTrait, + ModalWindowType, ResponseWindow, TextArea, TextLine, TextWindowTrait, WindowEvent, WindowKind, }; pub use crate::external as lumni; pub struct AppUi<'a> { - pub prompt: PromptWindow<'a>, + pub prompt: TextArea<'a>, pub response: ResponseWindow<'a>, pub command_line: CommandLine<'a>, pub primary_window: WindowKind, @@ -23,7 +24,7 @@ pub struct AppUi<'a> { impl AppUi<'_> { pub fn new(conversation_text: Option>) -> Self { Self { - prompt: PromptWindow::new(), + prompt: TextArea::new().with_borders(Borders::ALL), response: ResponseWindow::new(conversation_text), command_line: CommandLine::new(), primary_window: WindowKind::ResponseWindow, @@ -70,16 +71,9 @@ impl AppUi<'_> { Ok(()) } - pub fn needs_modal_update(&self, new_type: &ModalWindowType) -> bool { - match self.modal.as_ref() { - Some(modal) => *new_type != modal.get_type(), - None => true, - } - } - pub fn set_primary_window(&mut self, window_type: WindowKind) { self.primary_window = match window_type { - WindowKind::ResponseWindow | WindowKind::PromptWindow => { + WindowKind::ResponseWindow | WindowKind::EditorWindow => { window_type } _ => { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs index 76b9a78..95e2f54 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs @@ -5,12 +5,11 @@ mod text_document; mod text_window; mod window_config; -use crossterm::event::KeyCode; pub use cursor::MoveCursor; use lumni::api::error::ApplicationError; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; -pub use scroller::Scroller; +use ratatui::widgets::Borders; pub use text_display::LineType; pub use text_document::{ ReadDocument, ReadWriteDocument, SimpleString, TextDocumentTrait, TextLine, @@ -73,25 +72,30 @@ impl RectArea { } } -pub struct PromptWindow<'a> { +pub struct TextArea<'a> { base: TextWindow<'a, ReadWriteDocument>, } -impl<'a> TextWindowTrait<'a, ReadWriteDocument> for PromptWindow<'a> { +impl<'a> TextWindowTrait<'a, ReadWriteDocument> for TextArea<'a> { fn base(&mut self) -> &mut TextWindow<'a, ReadWriteDocument> { &mut self.base } } -impl PromptWindow<'_> { +impl TextArea<'_> { pub fn new() -> Self { - let mut window_type = WindowConfig::new(WindowKind::PromptWindow); + let mut window_type = WindowConfig::new(WindowKind::EditorWindow); window_type.set_window_status(WindowStatus::InActive); Self { base: TextWindow::new_read_write(window_type, None), } } + pub fn with_borders(mut self, borders: Borders) -> Self { + self.base.set_borders(borders); + self + } + pub fn next_window_status(&mut self) -> WindowEvent { let next_status = match self.window_status() { WindowStatus::Normal(_) => WindowStatus::Insert, @@ -172,35 +176,4 @@ impl CommandLine<'_> { self.mode = CommandLineMode::Normal; } } - - pub fn process_edit_input( - &mut self, - key_event: &KeyTrack, - ) -> Result { - // process input for editing text, return true if input was processed - match key_event.current_key().code { - KeyCode::Right => { - self.move_cursor(MoveCursor::Right(1)); - } - KeyCode::Left => { - self.move_cursor(MoveCursor::Left(1)); - } - KeyCode::Home => { - self.move_cursor(MoveCursor::StartOfLine); - } - KeyCode::End => { - self.move_cursor(MoveCursor::EndOfLine); - } - KeyCode::Backspace => { - self.text_delete_backspace()?; - } - KeyCode::Char(c) => { - self.text_insert_add(&c.to_string(), None).unwrap(); - } - _ => { - return Ok(false); // input not processed - } - } - Ok(true) // input processed - } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/mod.rs index d83de57..0fe584b 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/mod.rs @@ -16,12 +16,12 @@ use super::text_display::{ CodeBlock, CodeBlockLine, CodeBlockLineType, TextDisplay, }; use super::text_document::{ - ReadDocument, ReadWriteDocument, TextDocumentTrait, TextLine, TextWrapper, + ReadDocument, ReadWriteDocument, TextDocumentTrait, TextLine, }; use super::window_config::{ WindowConfig, WindowContent, WindowKind, WindowStatus, }; -use super::{LineType, RectArea}; +use super::{KeyTrack, LineType, RectArea}; use crate::external as lumni; #[derive(Debug, Clone)] @@ -29,6 +29,7 @@ pub struct TextWindow<'a, T: TextDocumentTrait> { area: RectArea, window_type: WindowConfig, scroller: Scroller, + borders: Borders, text_buffer: TextBuffer<'a, T>, } @@ -38,6 +39,7 @@ impl<'a, T: TextDocumentTrait> TextWindow<'a, T> { area: RectArea::default(), window_type, scroller: Scroller::new(), + borders: Borders::NONE, text_buffer: TextBuffer::new(document), } } @@ -46,6 +48,10 @@ impl<'a, T: TextDocumentTrait> TextWindow<'a, T> { self.window_type.window_status() } + pub fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + pub fn set_window_status(&mut self, status: WindowStatus) { if self.window_status() == status { return; // no change @@ -187,12 +193,12 @@ impl<'a, T: TextDocumentTrait> TextWindow<'a, T> { } pub fn widget<'b>(&'b mut self, area: &Rect) -> Paragraph<'b> { - let borders = self.window_type.borders(); - let (h_borders, v_borders) = match borders { + //let borders = self.window_type.borders(); + let (h_borders, v_borders) = match self.borders { Borders::ALL => (true, true), Borders::NONE => (false, false), _ => { - unimplemented!("Unsupported border type: {:?}", borders); + unimplemented!("Unsupported border type: {:?}", self.borders); } }; @@ -203,7 +209,7 @@ impl<'a, T: TextDocumentTrait> TextWindow<'a, T> { } let mut block = Block::default() - .borders(self.window_type.borders()) + .borders(self.borders) .border_style(self.window_type.border_style()) .padding(Padding::new(0, 0, 0, 0)); @@ -296,6 +302,11 @@ impl<'a> TextWindow<'a, ReadWriteDocument> { }; Self::new(window_type, document) } + + pub fn with_borders(&mut self, borders: Borders) -> &mut Self { + self.borders = borders; + self + } } impl<'a> TextWindow<'a, ReadDocument> { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/text_window_trait.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/text_window_trait.rs index da69908..c614f4e 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/text_window_trait.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_window/text_window_trait.rs @@ -1,11 +1,12 @@ +use crossterm::event::KeyCode; use lumni::api::error::ApplicationError; use ratatui::layout::Rect; use ratatui::style::Style; use ratatui::widgets::{Paragraph, ScrollbarState}; use super::{ - CodeBlock, LineType, MoveCursor, TextBuffer, TextDocumentTrait, TextWindow, - WindowContent, WindowKind, WindowStatus, + CodeBlock, KeyTrack, LineType, MoveCursor, TextBuffer, TextDocumentTrait, + TextWindow, WindowContent, WindowKind, WindowStatus, }; use crate::external as lumni; @@ -206,4 +207,35 @@ pub trait TextWindowTrait<'a, T: TextDocumentTrait> { fn set_status_inactive(&mut self) { self.set_window_status(WindowStatus::InActive); } + + fn process_edit_input( + &mut self, + key_event: &KeyTrack, + ) -> Result { + // process input for editing text, return true if input was processed + match key_event.current_key().code { + KeyCode::Right => { + self.move_cursor(MoveCursor::Right(1)); + } + KeyCode::Left => { + self.move_cursor(MoveCursor::Left(1)); + } + KeyCode::Home => { + self.move_cursor(MoveCursor::StartOfLine); + } + KeyCode::End => { + self.move_cursor(MoveCursor::EndOfLine); + } + KeyCode::Backspace => { + self.text_delete_backspace()?; + } + KeyCode::Char(c) => { + self.text_insert_add(&c.to_string(), None).unwrap(); + } + _ => { + return Ok(false); // input not processed + } + } + Ok(true) // input processed + } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/window_config.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/window_config.rs index 3a8b58f..c408896 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/window_config.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/window_config.rs @@ -20,7 +20,7 @@ pub enum WindowContent { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WindowKind { ResponseWindow, - PromptWindow, + EditorWindow, CommandLine, } @@ -50,7 +50,7 @@ impl WindowConfig { pub fn hint(&self) -> Option { match self.kind { - WindowKind::PromptWindow => match self.status { + WindowKind::EditorWindow => match self.status { WindowStatus::Normal(None) => Some( Title::from("press i to enter insert mode".dark_gray()) .alignment(Alignment::Right) @@ -77,7 +77,7 @@ impl WindowConfig { pub fn placeholder_text(&self) -> &str { match self.kind { WindowKind::ResponseWindow => "", - WindowKind::PromptWindow => match self.status { + WindowKind::EditorWindow => match self.status { WindowStatus::Normal(_) | WindowStatus::Background => { "Press i to enter insert mode" } @@ -89,14 +89,6 @@ impl WindowConfig { } } - pub fn borders(&self) -> Borders { - match self.kind { - WindowKind::ResponseWindow => Borders::NONE, - WindowKind::PromptWindow => Borders::ALL, - WindowKind::CommandLine => Borders::NONE, - } - } - pub fn style(&self) -> Style { match self.kind { WindowKind::CommandLine => Style::default(), @@ -132,7 +124,7 @@ impl WindowConfig { pub fn is_editable(&self) -> bool { match self.kind { WindowKind::ResponseWindow => false, - WindowKind::PromptWindow => true, + WindowKind::EditorWindow => true, WindowKind::CommandLine => true, } } diff --git a/lumni/src/handlers/object_store.rs b/lumni/src/handlers/object_store.rs index 001dc36..c633511 100644 --- a/lumni/src/handlers/object_store.rs +++ b/lumni/src/handlers/object_store.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use async_trait::async_trait; use log::debug; +use pkcs8::der::asn1::Int; use sqlparser::ast::{Expr, Query, SelectItem, SetExpr, Statement}; use sqlparser::dialect::GenericDialect; use sqlparser::parser::Parser; @@ -15,7 +16,7 @@ use crate::table::object_store::table_from_list_bucket; use crate::table::{FileObjectTable, Table, TableCallback}; use crate::{ BinaryCallbackWrapper, EnvironmentConfig, FileObjectFilter, IgnoreContents, - InternalError, ObjectStoreTable, ParsedUri, UriScheme, + InternalError, LumniError, ObjectStoreTable, ParsedUri, UriScheme, }; #[derive(Debug, Clone)] @@ -126,6 +127,18 @@ impl ObjectStore { } } } + + pub async fn head_object( + &self, + key: &str, + ) -> Result<(u16, HashMap<String, String>), InternalError> { + match self { + ObjectStore::S3Bucket(bucket) => bucket.head_object(key).await, + ObjectStore::LocalFsBucket(local_fs) => { + local_fs.head_object(key).await + } + } + } } #[async_trait(?Send)] @@ -442,7 +455,13 @@ impl ObjectStoreHandler { // uri does not point to a bucket or (virtual) directory // assume it to be a pointer to a database file (e.g. .sql, .parquet) return self - .query_object(&uri, config, query, callback) + //.query_object(&uri, config, query, callback) + .query_object( + &ParsedUri::from_uri(&uri, true), + config, + query, + callback, + ) .await; } _ => return result, // TODO: query should return Table @@ -457,17 +476,21 @@ impl ObjectStoreHandler { async fn query_object( &self, - _uri: &str, - _config: &EnvironmentConfig, + parsed_uri: &ParsedUri, + config: &EnvironmentConfig, _query: &Query, _callback: Option<Arc<dyn TableCallback>>, ) -> Result<Box<dyn Table>, InternalError> { // Logic to treat the URI as a database file and query it - - // This is a placeholder for the actual implementation. - Err(InternalError::InternalError( - "Querying object not implemented".to_string(), - )) + let object_store = + ObjectStore::new(&parsed_uri.to_string(), config.clone())?; + let _object_data = object_store + .head_object(parsed_uri.path.as_deref().unwrap_or("")) + .await?; + + // TODO: return file object_data as Table + let table = FileObjectTable::new(&None, None); + Ok(Box::new(table)) } } diff --git a/lumni/src/localfs/bucket.rs b/lumni/src/localfs/bucket.rs index 1ee0ea4..6465e75 100644 --- a/lumni/src/localfs/bucket.rs +++ b/lumni/src/localfs/bucket.rs @@ -4,6 +4,7 @@ use std::path::Path; use async_trait::async_trait; use super::get::get_object; +use super::head::head_object; use super::list::list_files; use crate::base::config::EnvironmentConfig; use crate::handlers::object_store::ObjectStoreTrait; @@ -86,8 +87,8 @@ impl ObjectStoreTrait for LocalFsBucket { &self, _key: &str, ) -> Result<(u16, HashMap<String, String>), InternalError> { - return Err(InternalError::InternalError( - "Not implemented".to_string(), - )); + let path = Path::new(&self.name); + head_object(path, _key).await?; + Ok((200, HashMap::new())) } } diff --git a/lumni/src/localfs/get.rs b/lumni/src/localfs/get.rs index 2faa948..8f53e25 100644 --- a/lumni/src/localfs/get.rs +++ b/lumni/src/localfs/get.rs @@ -1,5 +1,3 @@ -// localfs/get.rs - use std::fs; use std::io::Read; use std::path::Path; @@ -38,3 +36,16 @@ pub async fn get_object( ))) } } + +pub async fn head_object(path: &Path, key: &str) -> Result<(), InternalError> { + let object_path = path.join(key); + + if object_path.is_file() { + Ok(()) + } else { + Err(InternalError::NotFound(format!( + "Object not found for key: {}", + key + ))) + } +} diff --git a/lumni/src/localfs/head.rs b/lumni/src/localfs/head.rs new file mode 100644 index 0000000..f2f0227 --- /dev/null +++ b/lumni/src/localfs/head.rs @@ -0,0 +1,18 @@ +use std::path::Path; + +use crate::InternalError; + +pub async fn head_object(path: &Path, key: &str) -> Result<(), InternalError> { + let object_path = path.join(key); + // TODO: + // currently just check if file exists + // should return metadata + if object_path.is_file() { + Ok(()) + } else { + Err(InternalError::NotFound(format!( + "Object not found for key: {}", + key + ))) + } +} diff --git a/lumni/src/localfs/mod.rs b/lumni/src/localfs/mod.rs index 116ce3c..1a2b2b4 100644 --- a/lumni/src/localfs/mod.rs +++ b/lumni/src/localfs/mod.rs @@ -2,4 +2,5 @@ pub mod backend; mod bucket; mod get; +mod head; mod list;