Remove App implementation: delete TUI application logic, event handling, and related structures.
This commit is contained in:
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use owlen_core::{
|
||||
session::{SessionController, SessionOutcome},
|
||||
types::{ChatParameters, ChatResponse, Conversation, ModelInfo, Role},
|
||||
ui::{AppState, AutoScroll, FocusedPanel, InputMode},
|
||||
};
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use tokio::sync::mpsc;
|
||||
@@ -11,80 +12,6 @@ use uuid::Uuid;
|
||||
use crate::config;
|
||||
use crate::events::Event;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AppState {
|
||||
Running,
|
||||
Quit,
|
||||
}
|
||||
|
||||
pub struct AutoScroll {
|
||||
pub scroll: usize,
|
||||
pub content_len: usize,
|
||||
pub stick_to_bottom: bool,
|
||||
}
|
||||
|
||||
impl Default for AutoScroll {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scroll: 0,
|
||||
content_len: 0,
|
||||
stick_to_bottom: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AutoScroll {
|
||||
pub fn on_viewport(&mut self, viewport_h: usize) {
|
||||
let max = self.content_len.saturating_sub(viewport_h);
|
||||
if self.stick_to_bottom {
|
||||
self.scroll = max;
|
||||
} else {
|
||||
self.scroll = self.scroll.min(max);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_user_scroll(&mut self, delta: isize, viewport_h: usize) {
|
||||
let max = self.content_len.saturating_sub(viewport_h) as isize;
|
||||
let s = (self.scroll as isize + delta).clamp(0, max) as usize;
|
||||
self.scroll = s;
|
||||
self.stick_to_bottom = s as isize == max;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InputMode {
|
||||
Normal,
|
||||
Editing,
|
||||
ProviderSelection,
|
||||
ModelSelection,
|
||||
Help,
|
||||
Visual,
|
||||
Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FocusedPanel {
|
||||
Chat,
|
||||
Thinking,
|
||||
Input,
|
||||
}
|
||||
|
||||
impl fmt::Display for InputMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let label = match self {
|
||||
InputMode::Normal => "Normal",
|
||||
InputMode::Editing => "Editing",
|
||||
InputMode::ModelSelection => "Model",
|
||||
InputMode::ProviderSelection => "Provider",
|
||||
InputMode::Help => "Help",
|
||||
InputMode::Visual => "Visual",
|
||||
InputMode::Command => "Command",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Messages emitted by asynchronous streaming tasks
|
||||
#[derive(Debug)]
|
||||
@@ -1184,13 +1111,11 @@ impl ChatApp {
|
||||
pub fn scroll_half_page_down(&mut self) {
|
||||
match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
let delta = (self.viewport_height / 2) as isize;
|
||||
self.auto_scroll.on_user_scroll(delta, self.viewport_height);
|
||||
self.auto_scroll.scroll_half_page_down(self.viewport_height);
|
||||
}
|
||||
FocusedPanel::Thinking => {
|
||||
let viewport_height = self.thinking_viewport_height.max(1);
|
||||
let delta = (viewport_height / 2) as isize;
|
||||
self.thinking_scroll.on_user_scroll(delta, viewport_height);
|
||||
self.thinking_scroll.scroll_half_page_down(viewport_height);
|
||||
}
|
||||
FocusedPanel::Input => {}
|
||||
}
|
||||
@@ -1200,13 +1125,11 @@ impl ChatApp {
|
||||
pub fn scroll_half_page_up(&mut self) {
|
||||
match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
let delta = -((self.viewport_height / 2) as isize);
|
||||
self.auto_scroll.on_user_scroll(delta, self.viewport_height);
|
||||
self.auto_scroll.scroll_half_page_up(self.viewport_height);
|
||||
}
|
||||
FocusedPanel::Thinking => {
|
||||
let viewport_height = self.thinking_viewport_height.max(1);
|
||||
let delta = -((viewport_height / 2) as isize);
|
||||
self.thinking_scroll.on_user_scroll(delta, viewport_height);
|
||||
self.thinking_scroll.scroll_half_page_up(viewport_height);
|
||||
}
|
||||
FocusedPanel::Input => {}
|
||||
}
|
||||
@@ -1216,13 +1139,11 @@ impl ChatApp {
|
||||
pub fn scroll_full_page_down(&mut self) {
|
||||
match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
let delta = self.viewport_height as isize;
|
||||
self.auto_scroll.on_user_scroll(delta, self.viewport_height);
|
||||
self.auto_scroll.scroll_full_page_down(self.viewport_height);
|
||||
}
|
||||
FocusedPanel::Thinking => {
|
||||
let viewport_height = self.thinking_viewport_height.max(1);
|
||||
let delta = viewport_height as isize;
|
||||
self.thinking_scroll.on_user_scroll(delta, viewport_height);
|
||||
self.thinking_scroll.scroll_full_page_down(viewport_height);
|
||||
}
|
||||
FocusedPanel::Input => {}
|
||||
}
|
||||
@@ -1232,13 +1153,11 @@ impl ChatApp {
|
||||
pub fn scroll_full_page_up(&mut self) {
|
||||
match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
let delta = -(self.viewport_height as isize);
|
||||
self.auto_scroll.on_user_scroll(delta, self.viewport_height);
|
||||
self.auto_scroll.scroll_full_page_up(self.viewport_height);
|
||||
}
|
||||
FocusedPanel::Thinking => {
|
||||
let viewport_height = self.thinking_viewport_height.max(1);
|
||||
let delta = -(viewport_height as isize);
|
||||
self.thinking_scroll.on_user_scroll(delta, viewport_height);
|
||||
self.thinking_scroll.scroll_full_page_up(viewport_height);
|
||||
}
|
||||
FocusedPanel::Input => {}
|
||||
}
|
||||
@@ -1248,12 +1167,10 @@ impl ChatApp {
|
||||
pub fn jump_to_top(&mut self) {
|
||||
match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
self.auto_scroll.scroll = 0;
|
||||
self.auto_scroll.stick_to_bottom = false;
|
||||
self.auto_scroll.jump_to_top();
|
||||
}
|
||||
FocusedPanel::Thinking => {
|
||||
self.thinking_scroll.scroll = 0;
|
||||
self.thinking_scroll.stick_to_bottom = false;
|
||||
self.thinking_scroll.jump_to_top();
|
||||
}
|
||||
FocusedPanel::Input => {}
|
||||
}
|
||||
@@ -1263,13 +1180,11 @@ impl ChatApp {
|
||||
pub fn jump_to_bottom(&mut self) {
|
||||
match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
self.auto_scroll.stick_to_bottom = true;
|
||||
self.auto_scroll.on_viewport(self.viewport_height);
|
||||
self.auto_scroll.jump_to_bottom(self.viewport_height);
|
||||
}
|
||||
FocusedPanel::Thinking => {
|
||||
let viewport_height = self.thinking_viewport_height.max(1);
|
||||
self.thinking_scroll.stick_to_bottom = true;
|
||||
self.thinking_scroll.on_viewport(viewport_height);
|
||||
self.thinking_scroll.jump_to_bottom(viewport_height);
|
||||
}
|
||||
FocusedPanel::Input => {}
|
||||
}
|
||||
@@ -1576,94 +1491,17 @@ impl ChatApp {
|
||||
|
||||
fn find_next_word_boundary(&self, row: usize, col: usize) -> Option<usize> {
|
||||
let line = self.get_line_at_row(row)?;
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
|
||||
if col >= chars.len() {
|
||||
return Some(chars.len());
|
||||
}
|
||||
|
||||
let mut pos = col;
|
||||
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
|
||||
|
||||
// Skip current word
|
||||
if is_word_char(chars[pos]) {
|
||||
while pos < chars.len() && is_word_char(chars[pos]) {
|
||||
pos += 1;
|
||||
}
|
||||
} else {
|
||||
// Skip non-word characters
|
||||
while pos < chars.len() && !is_word_char(chars[pos]) {
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(pos)
|
||||
owlen_core::ui::find_next_word_boundary(&line, col)
|
||||
}
|
||||
|
||||
fn find_word_end(&self, row: usize, col: usize) -> Option<usize> {
|
||||
let line = self.get_line_at_row(row)?;
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
|
||||
if col >= chars.len() {
|
||||
return Some(chars.len());
|
||||
}
|
||||
|
||||
let mut pos = col;
|
||||
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
|
||||
|
||||
// If on a word character, move to end of current word
|
||||
if is_word_char(chars[pos]) {
|
||||
while pos < chars.len() && is_word_char(chars[pos]) {
|
||||
pos += 1;
|
||||
}
|
||||
// Move back one to be ON the last character
|
||||
if pos > 0 {
|
||||
pos -= 1;
|
||||
}
|
||||
} else {
|
||||
// Skip non-word characters
|
||||
while pos < chars.len() && !is_word_char(chars[pos]) {
|
||||
pos += 1;
|
||||
}
|
||||
// Now on first char of next word, move to its end
|
||||
while pos < chars.len() && is_word_char(chars[pos]) {
|
||||
pos += 1;
|
||||
}
|
||||
if pos > 0 {
|
||||
pos -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(pos)
|
||||
owlen_core::ui::find_word_end(&line, col)
|
||||
}
|
||||
|
||||
fn find_prev_word_boundary(&self, row: usize, col: usize) -> Option<usize> {
|
||||
let line = self.get_line_at_row(row)?;
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
|
||||
if col == 0 || chars.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let mut pos = col.min(chars.len());
|
||||
let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
|
||||
|
||||
// Move back one position first
|
||||
if pos > 0 {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
// Skip non-word characters
|
||||
while pos > 0 && !is_word_char(chars[pos]) {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
// Skip word characters to find start of word
|
||||
while pos > 0 && is_word_char(chars[pos - 1]) {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
Some(pos)
|
||||
owlen_core::ui::find_prev_word_boundary(&line, col)
|
||||
}
|
||||
|
||||
fn yank_from_panel(&self) -> Option<String> {
|
||||
@@ -1679,7 +1517,7 @@ impl ChatApp {
|
||||
};
|
||||
|
||||
let lines = self.get_rendered_lines();
|
||||
extract_text_from_selection(&lines, start_pos, end_pos)
|
||||
owlen_core::ui::extract_text_from_selection(&lines, start_pos, end_pos)
|
||||
}
|
||||
|
||||
pub fn update_thinking_from_last_message(&mut self) {
|
||||
@@ -1736,66 +1574,6 @@ impl ChatApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_text_from_selection(lines: &[String], start: (usize, usize), end: (usize, usize)) -> Option<String> {
|
||||
if lines.is_empty() || start.0 >= lines.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_row = start.0;
|
||||
let start_col = start.1;
|
||||
let end_row = end.0.min(lines.len() - 1);
|
||||
let end_col = end.1;
|
||||
|
||||
if start_row == end_row {
|
||||
// Single line selection
|
||||
let line = &lines[start_row];
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
let start_c = start_col.min(chars.len());
|
||||
let end_c = end_col.min(chars.len());
|
||||
|
||||
if start_c >= end_c {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selected: String = chars[start_c..end_c].iter().collect();
|
||||
Some(selected)
|
||||
} else {
|
||||
// Multi-line selection
|
||||
let mut result = Vec::new();
|
||||
|
||||
// First line: from start_col to end
|
||||
let first_line = &lines[start_row];
|
||||
let first_chars: Vec<char> = first_line.chars().collect();
|
||||
let start_c = start_col.min(first_chars.len());
|
||||
if start_c < first_chars.len() {
|
||||
result.push(first_chars[start_c..].iter().collect::<String>());
|
||||
}
|
||||
|
||||
// Middle lines: entire lines
|
||||
for row in (start_row + 1)..end_row {
|
||||
if row < lines.len() {
|
||||
result.push(lines[row].clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Last line: from start to end_col
|
||||
if end_row < lines.len() && end_row > start_row {
|
||||
let last_line = &lines[end_row];
|
||||
let last_chars: Vec<char> = last_line.chars().collect();
|
||||
let end_c = end_col.min(last_chars.len());
|
||||
if end_c > 0 {
|
||||
result.push(last_chars[..end_c].iter().collect::<String>());
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(result.join("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_textarea_defaults(textarea: &mut TextArea<'static>) {
|
||||
textarea.set_placeholder_text("Type your message here...");
|
||||
textarea.set_tab_length(4);
|
||||
|
||||
Reference in New Issue
Block a user