Add word wrapping and cursor mapping utilities to core library; integrate advanced text input support in TUI. Update dependencies accordingly.
This commit is contained in:
@@ -3,7 +3,9 @@ use owlen_core::{
|
||||
session::{SessionController, SessionOutcome},
|
||||
types::{ChatParameters, ChatResponse, Conversation, ModelInfo},
|
||||
};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use tokio::sync::mpsc;
|
||||
use tui_textarea::{Input, TextArea};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config;
|
||||
@@ -64,11 +66,15 @@ pub struct ChatApp {
|
||||
scroll: usize,
|
||||
session_tx: mpsc::UnboundedSender<SessionEvent>,
|
||||
streaming: HashSet<Uuid>,
|
||||
textarea: TextArea<'static>, // Advanced text input widget
|
||||
}
|
||||
|
||||
impl ChatApp {
|
||||
pub fn new(controller: SessionController) -> (Self, mpsc::UnboundedReceiver<SessionEvent>) {
|
||||
let (session_tx, session_rx) = mpsc::unbounded_channel();
|
||||
let mut textarea = TextArea::default();
|
||||
configure_textarea_defaults(&mut textarea);
|
||||
|
||||
let app = Self {
|
||||
controller,
|
||||
mode: InputMode::Normal,
|
||||
@@ -82,6 +88,7 @@ impl ChatApp {
|
||||
scroll: 0,
|
||||
session_tx,
|
||||
streaming: std::collections::HashSet::new(),
|
||||
textarea,
|
||||
};
|
||||
|
||||
(app, session_rx)
|
||||
@@ -146,6 +153,28 @@ impl ChatApp {
|
||||
self.controller.input_buffer_mut()
|
||||
}
|
||||
|
||||
pub fn textarea(&self) -> &TextArea<'static> {
|
||||
&self.textarea
|
||||
}
|
||||
|
||||
pub fn textarea_mut(&mut self) -> &mut TextArea<'static> {
|
||||
&mut self.textarea
|
||||
}
|
||||
|
||||
/// Sync textarea content to input buffer
|
||||
fn sync_textarea_to_buffer(&mut self) {
|
||||
let text = self.textarea.lines().join("\n");
|
||||
self.input_buffer_mut().set_text(text);
|
||||
}
|
||||
|
||||
/// Sync input buffer content to textarea
|
||||
fn sync_buffer_to_textarea(&mut self) {
|
||||
let text = self.input_buffer().text().to_string();
|
||||
let lines: Vec<String> = text.lines().map(|s| s.to_string()).collect();
|
||||
self.textarea = TextArea::new(lines);
|
||||
configure_textarea_defaults(&mut self.textarea);
|
||||
}
|
||||
|
||||
pub async fn initialize_models(&mut self) -> Result<()> {
|
||||
let config_model_name = self.controller.config().general.default_model.clone();
|
||||
let config_model_provider = self.controller.config().general.default_provider.clone();
|
||||
@@ -227,6 +256,7 @@ impl ChatApp {
|
||||
(KeyCode::Enter, KeyModifiers::NONE)
|
||||
| (KeyCode::Char('i'), KeyModifiers::NONE) => {
|
||||
self.mode = InputMode::Editing;
|
||||
self.sync_buffer_to_textarea();
|
||||
}
|
||||
(KeyCode::Up, KeyModifiers::NONE) => {
|
||||
self.scroll = self.scroll.saturating_add(1);
|
||||
@@ -241,56 +271,41 @@ impl ChatApp {
|
||||
},
|
||||
InputMode::Editing => match key.code {
|
||||
KeyCode::Esc if key.modifiers.is_empty() => {
|
||||
// Sync textarea content to input buffer before leaving edit mode
|
||||
self.sync_textarea_to_buffer();
|
||||
self.mode = InputMode::Normal;
|
||||
self.reset_status();
|
||||
}
|
||||
KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
self.input_buffer_mut().insert_char('\n');
|
||||
KeyCode::Char('j' | 'J') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.textarea.insert_newline();
|
||||
}
|
||||
KeyCode::Enter if key.modifiers.is_empty() => {
|
||||
// Send message and return to normal mode
|
||||
self.sync_textarea_to_buffer();
|
||||
self.try_send_message().await?;
|
||||
// Clear the textarea by setting it to empty
|
||||
self.textarea = TextArea::default();
|
||||
configure_textarea_defaults(&mut self.textarea);
|
||||
self.mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
self.input_buffer_mut().insert_char('\n');
|
||||
// Any Enter with modifiers keeps editing and inserts a newline via tui-textarea
|
||||
self.textarea.input(Input::from(key));
|
||||
}
|
||||
KeyCode::Char('j') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.input_buffer_mut().insert_char('\n');
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
self.input_buffer_mut().backspace();
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
self.input_buffer_mut().delete();
|
||||
}
|
||||
KeyCode::Left => {
|
||||
self.input_buffer_mut().move_left();
|
||||
}
|
||||
KeyCode::Right => {
|
||||
self.input_buffer_mut().move_right();
|
||||
}
|
||||
KeyCode::Home => {
|
||||
self.input_buffer_mut().move_home();
|
||||
}
|
||||
KeyCode::End => {
|
||||
self.input_buffer_mut().move_end();
|
||||
}
|
||||
KeyCode::Up => {
|
||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
// Navigate through input history
|
||||
self.input_buffer_mut().history_previous();
|
||||
self.sync_buffer_to_textarea();
|
||||
}
|
||||
KeyCode::Down => {
|
||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
// Navigate through input history
|
||||
self.input_buffer_mut().history_next();
|
||||
self.sync_buffer_to_textarea();
|
||||
}
|
||||
KeyCode::Char(c)
|
||||
if key.modifiers.is_empty()
|
||||
|| key.modifiers.contains(KeyModifiers::SHIFT) =>
|
||||
{
|
||||
self.input_buffer_mut().insert_char(c);
|
||||
_ => {
|
||||
// Let tui-textarea handle all other input
|
||||
self.textarea.input(Input::from(key));
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
self.input_buffer_mut().insert_tab();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::ProviderSelection => match key.code {
|
||||
KeyCode::Esc => {
|
||||
@@ -381,7 +396,11 @@ impl ChatApp {
|
||||
message_id,
|
||||
response,
|
||||
} => {
|
||||
let at_bottom = self.scroll == 0;
|
||||
self.controller.apply_stream_chunk(message_id, &response)?;
|
||||
if at_bottom {
|
||||
self.scroll_to_bottom();
|
||||
}
|
||||
if response.is_final {
|
||||
self.streaming.remove(&message_id);
|
||||
self.status = "Response complete".to_string();
|
||||
@@ -458,6 +477,8 @@ impl ChatApp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.scroll_to_bottom();
|
||||
|
||||
let message = self.controller.input_buffer_mut().commit_to_history();
|
||||
let mut parameters = ChatParameters::default();
|
||||
parameters.stream = self.controller.config().general.enable_streaming;
|
||||
@@ -538,6 +559,10 @@ impl ChatApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_to_bottom(&mut self) {
|
||||
self.scroll = 0;
|
||||
}
|
||||
|
||||
fn spawn_stream(&mut self, message_id: Uuid, mut stream: owlen_core::provider::ChatStream) {
|
||||
let sender = self.session_tx.clone();
|
||||
self.streaming.insert(message_id);
|
||||
@@ -569,3 +594,17 @@ impl ChatApp {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_textarea_defaults(textarea: &mut TextArea<'static>) {
|
||||
textarea.set_placeholder_text("Type your message here...");
|
||||
textarea.set_tab_length(4);
|
||||
|
||||
textarea.set_style(
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::UNDERLINED)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
.remove_modifier(Modifier::BOLD),
|
||||
);
|
||||
textarea.set_cursor_style(Style::default());
|
||||
textarea.set_cursor_line_style(Style::default());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user