Remove App implementation: delete TUI application logic, event handling, and related structures.

This commit is contained in:
2025-09-30 02:40:20 +02:00
parent a5727c0a1d
commit 54bcabd53d
12 changed files with 450 additions and 3299 deletions

View File

@@ -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);