user: InteractScreen extracted, all screens use ScreenView trait
InteractScreen in chat.rs owns conversation/autonomous/tools panes, textarea, input history, scroll state. App is now just shared state (status, sampling params, agent_state, channel_status, idle_info). Event loop holds InteractScreen separately for UiMessage routing. Overlay screens (F2-F5) in screens vec. F-key switching preserves state across screen changes. handle_ui_message moved from App to InteractScreen. handle_key split: global keys on App, screen keys in tick(). draw dispatch eliminated — each screen draws itself. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
8418bc9bc9
commit
68f115b880
8 changed files with 276 additions and 259 deletions
|
|
@ -1,5 +1,3 @@
|
||||||
#![feature(coroutines, coroutine_trait)]
|
|
||||||
|
|
||||||
// consciousness — unified crate for memory, agents, and subconscious processes
|
// consciousness — unified crate for memory, agents, and subconscious processes
|
||||||
//
|
//
|
||||||
// thought/ — shared cognitive substrate (tools, context, memory ops)
|
// thought/ — shared cognitive substrate (tools, context, memory ops)
|
||||||
|
|
|
||||||
250
src/user/chat.rs
250
src/user/chat.rs
|
|
@ -9,15 +9,225 @@ use ratatui::{
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
Frame,
|
Frame,
|
||||||
|
crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, MouseButton},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ActivePane, App, Marker, PaneState, screen_legend};
|
use super::{
|
||||||
|
ActivePane, App, HotkeyAction, Marker, PaneState, ScreenAction, ScreenView,
|
||||||
|
new_textarea, screen_legend,
|
||||||
|
};
|
||||||
|
use crate::user::ui_channel::{UiMessage, StreamTarget};
|
||||||
|
|
||||||
impl App {
|
pub(crate) struct InteractScreen {
|
||||||
|
pub(crate) autonomous: PaneState,
|
||||||
|
pub(crate) conversation: PaneState,
|
||||||
|
pub(crate) tools: PaneState,
|
||||||
|
pub(crate) textarea: tui_textarea::TextArea<'static>,
|
||||||
|
pub(crate) input_history: Vec<String>,
|
||||||
|
pub(crate) history_index: Option<usize>,
|
||||||
|
pub(crate) active_pane: ActivePane,
|
||||||
|
pub(crate) pane_areas: [Rect; 3],
|
||||||
|
pub(crate) needs_assistant_marker: bool,
|
||||||
|
pub(crate) turn_started: Option<std::time::Instant>,
|
||||||
|
pub(crate) call_started: Option<std::time::Instant>,
|
||||||
|
pub(crate) call_timeout_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractScreen {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
autonomous: PaneState::new(true),
|
||||||
|
conversation: PaneState::new(true),
|
||||||
|
tools: PaneState::new(false),
|
||||||
|
textarea: new_textarea(vec![String::new()]),
|
||||||
|
input_history: Vec::new(),
|
||||||
|
history_index: None,
|
||||||
|
active_pane: ActivePane::Conversation,
|
||||||
|
pane_areas: [Rect::default(); 3],
|
||||||
|
needs_assistant_marker: false,
|
||||||
|
turn_started: None,
|
||||||
|
call_started: None,
|
||||||
|
call_timeout_secs: 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a UiMessage — update pane state.
|
||||||
|
pub fn handle_ui_message(&mut self, msg: &UiMessage, app: &mut App) {
|
||||||
|
match msg {
|
||||||
|
UiMessage::TextDelta(text, target) => match target {
|
||||||
|
StreamTarget::Conversation => {
|
||||||
|
if self.needs_assistant_marker {
|
||||||
|
self.conversation.pending_marker = Marker::Assistant;
|
||||||
|
self.needs_assistant_marker = false;
|
||||||
|
}
|
||||||
|
self.conversation.current_color = Color::Reset;
|
||||||
|
self.conversation.append_text(text);
|
||||||
|
}
|
||||||
|
StreamTarget::Autonomous => {
|
||||||
|
self.autonomous.current_color = Color::Reset;
|
||||||
|
self.autonomous.append_text(text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UiMessage::UserInput(text) => {
|
||||||
|
self.conversation.push_line_with_marker(text.clone(), Color::Cyan, Marker::User);
|
||||||
|
self.turn_started = Some(std::time::Instant::now());
|
||||||
|
self.needs_assistant_marker = true;
|
||||||
|
app.status.turn_tools = 0;
|
||||||
|
}
|
||||||
|
UiMessage::ToolCall { name, args_summary } => {
|
||||||
|
app.status.turn_tools += 1;
|
||||||
|
let line = if args_summary.is_empty() { format!("[{}]", name) }
|
||||||
|
else { format!("[{}] {}", name, args_summary) };
|
||||||
|
self.tools.push_line(line, Color::Yellow);
|
||||||
|
}
|
||||||
|
UiMessage::ToolResult { result, .. } => {
|
||||||
|
for line in result.lines() {
|
||||||
|
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
||||||
|
}
|
||||||
|
self.tools.push_line(String::new(), Color::Reset);
|
||||||
|
}
|
||||||
|
UiMessage::DmnAnnotation(text) => {
|
||||||
|
self.autonomous.push_line(text.clone(), Color::Yellow);
|
||||||
|
self.turn_started = Some(std::time::Instant::now());
|
||||||
|
self.needs_assistant_marker = true;
|
||||||
|
app.status.turn_tools = 0;
|
||||||
|
}
|
||||||
|
UiMessage::StatusUpdate(info) => {
|
||||||
|
if !info.dmn_state.is_empty() {
|
||||||
|
app.status.dmn_state = info.dmn_state.clone();
|
||||||
|
app.status.dmn_turns = info.dmn_turns;
|
||||||
|
app.status.dmn_max_turns = info.dmn_max_turns;
|
||||||
|
}
|
||||||
|
if info.prompt_tokens > 0 { app.status.prompt_tokens = info.prompt_tokens; }
|
||||||
|
if !info.model.is_empty() { app.status.model = info.model.clone(); }
|
||||||
|
if !info.context_budget.is_empty() { app.status.context_budget = info.context_budget.clone(); }
|
||||||
|
}
|
||||||
|
UiMessage::Activity(text) => {
|
||||||
|
if text.is_empty() {
|
||||||
|
self.call_started = None;
|
||||||
|
} else if app.activity.is_empty() || self.call_started.is_none() {
|
||||||
|
self.call_started = Some(std::time::Instant::now());
|
||||||
|
self.call_timeout_secs = crate::config::get().api_stream_timeout_secs;
|
||||||
|
}
|
||||||
|
app.activity = text.clone();
|
||||||
|
}
|
||||||
|
UiMessage::Reasoning(text) => {
|
||||||
|
self.autonomous.current_color = Color::DarkGray;
|
||||||
|
self.autonomous.append_text(text);
|
||||||
|
}
|
||||||
|
UiMessage::Debug(text) => {
|
||||||
|
self.tools.push_line(format!("[debug] {}", text), Color::DarkGray);
|
||||||
|
}
|
||||||
|
UiMessage::Info(text) => {
|
||||||
|
self.conversation.push_line(text.clone(), Color::Cyan);
|
||||||
|
}
|
||||||
|
UiMessage::ContextInfoUpdate(info) => { app.context_info = Some(info.clone()); }
|
||||||
|
UiMessage::AgentUpdate(agents) => { app.agent_state = agents.clone(); }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_active_up(&mut self, n: u16) {
|
||||||
|
match self.active_pane {
|
||||||
|
ActivePane::Autonomous => self.autonomous.scroll_up(n),
|
||||||
|
ActivePane::Conversation => self.conversation.scroll_up(n),
|
||||||
|
ActivePane::Tools => self.tools.scroll_up(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_active_down(&mut self, n: u16) {
|
||||||
|
match self.active_pane {
|
||||||
|
ActivePane::Autonomous => self.autonomous.scroll_down(n),
|
||||||
|
ActivePane::Conversation => self.conversation.scroll_down(n),
|
||||||
|
ActivePane::Tools => self.tools.scroll_down(n),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_mouse(&mut self, mouse: MouseEvent) {
|
||||||
|
match mouse.kind {
|
||||||
|
MouseEventKind::ScrollUp => self.scroll_active_up(3),
|
||||||
|
MouseEventKind::ScrollDown => self.scroll_active_down(3),
|
||||||
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
|
let (x, y) = (mouse.column, mouse.row);
|
||||||
|
for (i, area) in self.pane_areas.iter().enumerate() {
|
||||||
|
if x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height {
|
||||||
|
self.active_pane = match i { 0 => ActivePane::Autonomous, 1 => ActivePane::Conversation, _ => ActivePane::Tools };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenView for InteractScreen {
|
||||||
|
fn label(&self) -> &'static str { "interact" }
|
||||||
|
|
||||||
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
|
key: Option<KeyEvent>, app: &mut App) -> Option<ScreenAction> {
|
||||||
|
// Handle keys
|
||||||
|
if let Some(key) = key {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => return Some(ScreenAction::Hotkey(HotkeyAction::Interrupt)),
|
||||||
|
KeyCode::Enter if !key.modifiers.contains(KeyModifiers::ALT) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||||
|
let input: String = self.textarea.lines().join("\n");
|
||||||
|
if !input.is_empty() {
|
||||||
|
if self.input_history.last().map_or(true, |h| h != &input) {
|
||||||
|
self.input_history.push(input.clone());
|
||||||
|
}
|
||||||
|
self.history_index = None;
|
||||||
|
// TODO: push to submitted via app or return action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => self.scroll_active_up(3),
|
||||||
|
KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => self.scroll_active_down(3),
|
||||||
|
KeyCode::Up => {
|
||||||
|
if !self.input_history.is_empty() {
|
||||||
|
let idx = match self.history_index { None => self.input_history.len() - 1, Some(i) => i.saturating_sub(1) };
|
||||||
|
self.history_index = Some(idx);
|
||||||
|
let mut ta = new_textarea(self.input_history[idx].lines().map(String::from).collect());
|
||||||
|
ta.move_cursor(tui_textarea::CursorMove::End);
|
||||||
|
self.textarea = ta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Some(idx) = self.history_index {
|
||||||
|
if idx + 1 < self.input_history.len() {
|
||||||
|
self.history_index = Some(idx + 1);
|
||||||
|
let mut ta = new_textarea(self.input_history[idx + 1].lines().map(String::from).collect());
|
||||||
|
ta.move_cursor(tui_textarea::CursorMove::End);
|
||||||
|
self.textarea = ta;
|
||||||
|
} else {
|
||||||
|
self.history_index = None;
|
||||||
|
self.textarea = new_textarea(vec![String::new()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::PageUp => self.scroll_active_up(10),
|
||||||
|
KeyCode::PageDown => self.scroll_active_down(10),
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.active_pane = match self.active_pane {
|
||||||
|
ActivePane::Autonomous => ActivePane::Tools,
|
||||||
|
ActivePane::Tools => ActivePane::Conversation,
|
||||||
|
ActivePane::Conversation => ActivePane::Autonomous,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => { self.textarea.input(key); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
self.draw_main(frame, area, app);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractScreen {
|
||||||
/// Draw the main (F1) screen — four-pane layout with status bar.
|
/// Draw the main (F1) screen — four-pane layout with status bar.
|
||||||
pub(crate) fn draw_main(&mut self, frame: &mut Frame, size: Rect) {
|
pub(crate) fn draw_main(&mut self, frame: &mut Frame, size: Rect, app: &App) {
|
||||||
// Main layout: content area + active tools overlay + status bar
|
// Main layout: content area + active tools overlay + status bar
|
||||||
let active_tools = self.active_tools.lock().unwrap();
|
let active_tools = app.active_tools.lock().unwrap();
|
||||||
let tool_lines = active_tools.len() as u16;
|
let tool_lines = active_tools.len() as u16;
|
||||||
let main_chunks = Layout::default()
|
let main_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
|
|
@ -124,47 +334,47 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw status bar with live activity indicator
|
// Draw status bar with live activity indicator
|
||||||
let timer = if !self.activity.is_empty() {
|
let timer = if !app.activity.is_empty() {
|
||||||
let total = self.turn_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
|
let total = self.turn_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
|
||||||
let call = self.call_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
|
let call = self.call_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
|
||||||
format!(" {}s, {}/{}s", total, call, self.call_timeout_secs)
|
format!(" {}s, {}/{}s", total, call, self.call_timeout_secs)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
let tools_info = if self.status.turn_tools > 0 {
|
let tools_info = if app.status.turn_tools > 0 {
|
||||||
format!(" ({}t)", self.status.turn_tools)
|
format!(" ({}t)", app.status.turn_tools)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
let activity_part = if self.activity.is_empty() {
|
let activity_part = if app.activity.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!(" | {}{}{}", self.activity, tools_info, timer)
|
format!(" | {}{}{}", app.activity, tools_info, timer)
|
||||||
};
|
};
|
||||||
|
|
||||||
let budget_part = if self.status.context_budget.is_empty() {
|
let budget_part = if app.status.context_budget.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
format!(" [{}]", self.status.context_budget)
|
format!(" [{}]", app.status.context_budget)
|
||||||
};
|
};
|
||||||
|
|
||||||
let left_status = format!(
|
let left_status = format!(
|
||||||
" {} | {}/{} dmn | {}K tok in{}{}",
|
" {} | {}/{} dmn | {}K tok in{}{}",
|
||||||
self.status.dmn_state,
|
app.status.dmn_state,
|
||||||
self.status.dmn_turns,
|
app.status.dmn_turns,
|
||||||
self.status.dmn_max_turns,
|
app.status.dmn_max_turns,
|
||||||
self.status.prompt_tokens / 1000,
|
app.status.prompt_tokens / 1000,
|
||||||
budget_part,
|
budget_part,
|
||||||
activity_part,
|
activity_part,
|
||||||
);
|
);
|
||||||
|
|
||||||
let proc_indicator = if self.running_processes > 0 {
|
let proc_indicator = if app.running_processes > 0 {
|
||||||
format!(" {}proc", self.running_processes)
|
format!(" {}proc", app.running_processes)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
let reason_indicator = if self.reasoning_effort != "none" {
|
let reason_indicator = if app.reasoning_effort != "none" {
|
||||||
format!(" reason:{}", self.reasoning_effort)
|
format!(" reason:{}", app.reasoning_effort)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
@ -172,7 +382,7 @@ impl App {
|
||||||
"{}{} ^P:pause ^R:reason ^K:kill | {} ",
|
"{}{} ^P:pause ^R:reason ^K:kill | {} ",
|
||||||
reason_indicator,
|
reason_indicator,
|
||||||
proc_indicator,
|
proc_indicator,
|
||||||
self.status.model,
|
app.status.model,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pad the middle to fill the status bar
|
// Pad the middle to fill the status bar
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ impl ScreenView for ConsciousScreen {
|
||||||
fn label(&self) -> &'static str { "conscious" }
|
fn label(&self) -> &'static str { "conscious" }
|
||||||
|
|
||||||
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
|
key: Option<KeyEvent>, app: &mut App) -> Option<ScreenAction> {
|
||||||
// Handle keys
|
// Handle keys
|
||||||
if let Some(key) = key {
|
if let Some(key) = key {
|
||||||
let context_state = self.read_context_state(app);
|
let context_state = self.read_context_state(app);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use tokio::sync::Mutex;
|
||||||
use crate::agent::Agent;
|
use crate::agent::Agent;
|
||||||
use crate::agent::api::ApiClient;
|
use crate::agent::api::ApiClient;
|
||||||
use crate::mind::MindCommand;
|
use crate::mind::MindCommand;
|
||||||
use crate::user::{self as tui, HotkeyAction};
|
use crate::user::{self as tui, HotkeyAction, ScreenView};
|
||||||
use crate::user::ui_channel::{self, UiMessage};
|
use crate::user::ui_channel::{self, UiMessage};
|
||||||
|
|
||||||
// ── Slash commands ─────────────────────────────────────────────
|
// ── Slash commands ─────────────────────────────────────────────
|
||||||
|
|
@ -381,14 +381,16 @@ pub async fn run(
|
||||||
}
|
}
|
||||||
let notify_rx = crate::thalamus::channels::subscribe_all();
|
let notify_rx = crate::thalamus::channels::subscribe_all();
|
||||||
|
|
||||||
// Overlay screens (F2-F5). Index 0 = no overlay (interact).
|
// InteractScreen held separately for UiMessage routing
|
||||||
|
let mut interact = crate::user::chat::InteractScreen::new();
|
||||||
|
// Overlay screens: F2=conscious, F3=subconscious, F4=unconscious, F5=thalamus
|
||||||
let mut screens: Vec<Box<dyn tui::ScreenView>> = vec![
|
let mut screens: Vec<Box<dyn tui::ScreenView>> = vec![
|
||||||
Box::new(crate::user::context::ConsciousScreen::new()),
|
Box::new(crate::user::context::ConsciousScreen::new()),
|
||||||
Box::new(crate::user::subconscious::SubconsciousScreen::new()),
|
Box::new(crate::user::subconscious::SubconsciousScreen::new()),
|
||||||
Box::new(crate::user::unconscious::UnconsciousScreen::new()),
|
Box::new(crate::user::unconscious::UnconsciousScreen::new()),
|
||||||
Box::new(crate::user::thalamus::ThalamusScreen::new()),
|
Box::new(crate::user::thalamus::ThalamusScreen::new()),
|
||||||
];
|
];
|
||||||
let mut active_screen: usize = 0; // 0 = interact, 1-4 = overlay index + 1
|
let mut active_screen: usize = 0; // 0 = interact, 1-4 = overlay
|
||||||
|
|
||||||
let mut terminal = tui::init_terminal()?;
|
let mut terminal = tui::init_terminal()?;
|
||||||
let mut reader = EventStream::new();
|
let mut reader = EventStream::new();
|
||||||
|
|
@ -403,9 +405,11 @@ pub async fn run(
|
||||||
|
|
||||||
let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into()));
|
let _ = ui_tx.send(UiMessage::Info("consciousness v0.3 (tui)".into()));
|
||||||
|
|
||||||
// Initial render — don't wait for Mind to init
|
// Initial render
|
||||||
app.drain_messages(&mut ui_rx);
|
terminal.draw(|f| {
|
||||||
terminal.draw(|f| app.draw(f))?;
|
let area = f.area();
|
||||||
|
interact.tick(f, area, None, &mut app);
|
||||||
|
})?;
|
||||||
|
|
||||||
// Replay conversation after Mind init completes (non-blocking check)
|
// Replay conversation after Mind init completes (non-blocking check)
|
||||||
let mut startup_done = false;
|
let mut startup_done = false;
|
||||||
|
|
@ -423,8 +427,8 @@ pub async fn run(
|
||||||
// F-keys switch screens
|
// F-keys switch screens
|
||||||
if let ratatui::crossterm::event::KeyCode::F(n) = key.code {
|
if let ratatui::crossterm::event::KeyCode::F(n) = key.code {
|
||||||
active_screen = match n {
|
active_screen = match n {
|
||||||
1 => 0,
|
1 => 0, // interact
|
||||||
n @ 2..=5 => n as usize - 1,
|
n @ 2..=5 if (n as usize - 2) < screens.len() => n as usize - 1,
|
||||||
_ => active_screen,
|
_ => active_screen,
|
||||||
};
|
};
|
||||||
if active_screen == 4 { // thalamus — refresh channels
|
if active_screen == 4 { // thalamus — refresh channels
|
||||||
|
|
@ -438,25 +442,18 @@ pub async fn run(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App handles global keys (Ctrl combos) + interact keys
|
// Global keys (Ctrl combos)
|
||||||
if active_screen == 0 {
|
app.handle_global_key(key);
|
||||||
app.handle_key(key);
|
|
||||||
} else {
|
|
||||||
// Global keys only
|
|
||||||
app.handle_key(key);
|
|
||||||
// Screen gets the key on next render tick
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store pending key for active overlay screen
|
// Store pending key for active overlay screen
|
||||||
pending_key = Some(key);
|
pending_key = Some(key);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
Some(Ok(Event::Mouse(mouse))) => {
|
Some(Ok(Event::Mouse(_mouse))) => {
|
||||||
app.handle_mouse(mouse);
|
// TODO: route to active screen
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
Some(Ok(Event::Resize(w, h))) => {
|
Some(Ok(Event::Resize(_w, _h))) => {
|
||||||
app.handle_resize(w, h);
|
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
|
@ -506,7 +503,7 @@ pub async fn run(
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(msg) = ui_rx.recv() => {
|
Some(msg) = ui_rx.recv() => {
|
||||||
app.handle_ui_message(msg);
|
interact.handle_ui_message(&msg, &mut app);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -540,7 +537,9 @@ pub async fn run(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.drain_messages(&mut ui_rx) {
|
// Drain UiMessages to interact screen
|
||||||
|
while let Ok(msg) = ui_rx.try_recv() {
|
||||||
|
interact.handle_ui_message(&msg, &mut app);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,12 +547,15 @@ pub async fn run(
|
||||||
let key = pending_key.take();
|
let key = pending_key.take();
|
||||||
let mut screen_action = None;
|
let mut screen_action = None;
|
||||||
if active_screen == 0 {
|
if active_screen == 0 {
|
||||||
terminal.draw(|f| app.draw(f))?;
|
terminal.draw(|f| {
|
||||||
|
let area = f.area();
|
||||||
|
screen_action = interact.tick(f, area, key, &mut app);
|
||||||
|
})?;
|
||||||
} else {
|
} else {
|
||||||
let screen = &mut screens[active_screen - 1];
|
let screen = &mut screens[active_screen - 1];
|
||||||
terminal.draw(|f| {
|
terminal.draw(|f| {
|
||||||
let area = f.area();
|
let area = f.area();
|
||||||
screen_action = screen.tick(f, area, key, &app);
|
screen_action = screen.tick(f, area, key, &mut app);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
if let Some(action) = screen_action {
|
if let Some(action) = screen_action {
|
||||||
|
|
|
||||||
225
src/user/mod.rs
225
src/user/mod.rs
|
|
@ -15,20 +15,18 @@ pub mod thalamus;
|
||||||
// --- TUI infrastructure (moved from tui/mod.rs) ---
|
// --- TUI infrastructure (moved from tui/mod.rs) ---
|
||||||
|
|
||||||
use ratatui::crossterm::{
|
use ratatui::crossterm::{
|
||||||
event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, MouseButton},
|
event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
|
||||||
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
ExecutableCommand,
|
ExecutableCommand,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::Rect,
|
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
Frame, Terminal,
|
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crate::user::ui_channel::{ContextInfo, SharedContextState, StatusInfo, UiMessage};
|
use crate::user::ui_channel::{ContextInfo, SharedContextState, StatusInfo};
|
||||||
|
|
||||||
/// Build the screen legend from the screen table.
|
/// Build the screen legend from the screen table.
|
||||||
pub(crate) fn screen_legend() -> String {
|
pub(crate) fn screen_legend() -> String {
|
||||||
|
|
@ -246,7 +244,7 @@ pub enum ScreenAction {
|
||||||
/// A screen that can draw itself and handle input.
|
/// A screen that can draw itself and handle input.
|
||||||
pub(crate) trait ScreenView: Send {
|
pub(crate) trait ScreenView: Send {
|
||||||
fn tick(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect,
|
fn tick(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect,
|
||||||
key: Option<ratatui::crossterm::event::KeyEvent>, app: &App) -> Option<ScreenAction>;
|
key: Option<ratatui::crossterm::event::KeyEvent>, app: &mut App) -> Option<ScreenAction>;
|
||||||
fn label(&self) -> &'static str;
|
fn label(&self) -> &'static str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,29 +274,17 @@ pub(crate) struct ChannelStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub(crate) autonomous: PaneState,
|
|
||||||
pub(crate) conversation: PaneState,
|
|
||||||
pub(crate) tools: PaneState,
|
|
||||||
pub(crate) status: StatusInfo,
|
pub(crate) status: StatusInfo,
|
||||||
pub(crate) activity: String,
|
pub(crate) activity: String,
|
||||||
pub(crate) turn_started: Option<std::time::Instant>,
|
|
||||||
pub(crate) call_started: Option<std::time::Instant>,
|
|
||||||
pub(crate) call_timeout_secs: u64,
|
|
||||||
pub(crate) needs_assistant_marker: bool,
|
|
||||||
pub running_processes: u32,
|
pub running_processes: u32,
|
||||||
pub reasoning_effort: String,
|
pub reasoning_effort: String,
|
||||||
pub temperature: f32,
|
pub temperature: f32,
|
||||||
pub top_p: f32,
|
pub top_p: f32,
|
||||||
pub top_k: u32,
|
pub top_k: u32,
|
||||||
pub(crate) active_tools: crate::user::ui_channel::SharedActiveTools,
|
pub(crate) active_tools: crate::user::ui_channel::SharedActiveTools,
|
||||||
pub(crate) active_pane: ActivePane,
|
|
||||||
pub textarea: tui_textarea::TextArea<'static>,
|
|
||||||
input_history: Vec<String>,
|
|
||||||
history_index: Option<usize>,
|
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
pub submitted: Vec<String>,
|
pub submitted: Vec<String>,
|
||||||
pub hotkey_actions: Vec<HotkeyAction>,
|
pub hotkey_actions: Vec<HotkeyAction>,
|
||||||
pub(crate) pane_areas: [Rect; 3],
|
|
||||||
pub(crate) context_info: Option<ContextInfo>,
|
pub(crate) context_info: Option<ContextInfo>,
|
||||||
pub(crate) shared_context: SharedContextState,
|
pub(crate) shared_context: SharedContextState,
|
||||||
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
|
pub(crate) agent_state: Vec<crate::subconscious::subconscious::AgentSnapshot>,
|
||||||
|
|
@ -309,218 +295,39 @@ pub struct App {
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(model: String, shared_context: SharedContextState, active_tools: crate::user::ui_channel::SharedActiveTools) -> Self {
|
pub fn new(model: String, shared_context: SharedContextState, active_tools: crate::user::ui_channel::SharedActiveTools) -> Self {
|
||||||
Self {
|
Self {
|
||||||
autonomous: PaneState::new(true),
|
|
||||||
conversation: PaneState::new(true),
|
|
||||||
tools: PaneState::new(false),
|
|
||||||
status: StatusInfo {
|
status: StatusInfo {
|
||||||
dmn_state: "resting".into(), dmn_turns: 0, dmn_max_turns: 20,
|
dmn_state: "resting".into(), dmn_turns: 0, dmn_max_turns: 20,
|
||||||
prompt_tokens: 0, completion_tokens: 0, model,
|
prompt_tokens: 0, completion_tokens: 0, model,
|
||||||
turn_tools: 0, context_budget: String::new(),
|
turn_tools: 0, context_budget: String::new(),
|
||||||
},
|
},
|
||||||
activity: String::new(),
|
activity: String::new(),
|
||||||
turn_started: None, call_started: None, call_timeout_secs: 60,
|
running_processes: 0,
|
||||||
needs_assistant_marker: false, running_processes: 0,
|
|
||||||
reasoning_effort: "none".to_string(),
|
reasoning_effort: "none".to_string(),
|
||||||
temperature: 0.6,
|
temperature: 0.6,
|
||||||
top_p: 0.95,
|
top_p: 0.95,
|
||||||
top_k: 20,
|
top_k: 20,
|
||||||
active_tools, active_pane: ActivePane::Conversation,
|
active_tools,
|
||||||
textarea: new_textarea(vec![String::new()]),
|
|
||||||
input_history: Vec::new(), history_index: None,
|
|
||||||
should_quit: false, submitted: Vec::new(), hotkey_actions: Vec::new(),
|
should_quit: false, submitted: Vec::new(), hotkey_actions: Vec::new(),
|
||||||
pane_areas: [Rect::default(); 3],
|
|
||||||
context_info: None, shared_context,
|
context_info: None, shared_context,
|
||||||
agent_state: Vec::new(),
|
agent_state: Vec::new(),
|
||||||
channel_status: Vec::new(), idle_info: None,
|
channel_status: Vec::new(), idle_info: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drain_messages(&mut self, rx: &mut crate::user::ui_channel::UiReceiver) -> bool {
|
pub fn handle_global_key_old(&mut self, _key: KeyEvent) -> bool { false } // placeholder
|
||||||
let mut any = false;
|
|
||||||
while let Ok(msg) = rx.try_recv() {
|
|
||||||
self.handle_ui_message(msg);
|
|
||||||
any = true;
|
|
||||||
}
|
|
||||||
any
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_ui_message(&mut self, msg: UiMessage) {
|
/// Handle global keys only (Ctrl combos).
|
||||||
use crate::user::ui_channel::StreamTarget;
|
pub fn handle_global_key(&mut self, key: KeyEvent) -> bool {
|
||||||
match msg {
|
|
||||||
UiMessage::TextDelta(text, target) => match target {
|
|
||||||
StreamTarget::Conversation => {
|
|
||||||
if self.needs_assistant_marker {
|
|
||||||
self.conversation.pending_marker = Marker::Assistant;
|
|
||||||
self.needs_assistant_marker = false;
|
|
||||||
}
|
|
||||||
self.conversation.current_color = Color::Reset;
|
|
||||||
self.conversation.append_text(&text);
|
|
||||||
}
|
|
||||||
StreamTarget::Autonomous => {
|
|
||||||
self.autonomous.current_color = Color::Reset;
|
|
||||||
self.autonomous.append_text(&text);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UiMessage::UserInput(text) => {
|
|
||||||
self.conversation.push_line_with_marker(text, Color::Cyan, Marker::User);
|
|
||||||
self.turn_started = Some(std::time::Instant::now());
|
|
||||||
self.needs_assistant_marker = true;
|
|
||||||
self.status.turn_tools = 0;
|
|
||||||
}
|
|
||||||
UiMessage::ToolCall { name, args_summary } => {
|
|
||||||
self.status.turn_tools += 1;
|
|
||||||
let line = if args_summary.is_empty() { format!("[{}]", name) }
|
|
||||||
else { format!("[{}] {}", name, args_summary) };
|
|
||||||
self.tools.push_line(line, Color::Yellow);
|
|
||||||
}
|
|
||||||
UiMessage::ToolResult { name: _, result } => {
|
|
||||||
for line in result.lines() {
|
|
||||||
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
|
||||||
}
|
|
||||||
self.tools.push_line(String::new(), Color::Reset);
|
|
||||||
}
|
|
||||||
UiMessage::DmnAnnotation(text) => {
|
|
||||||
self.autonomous.push_line(text, Color::Yellow);
|
|
||||||
self.turn_started = Some(std::time::Instant::now());
|
|
||||||
self.needs_assistant_marker = true;
|
|
||||||
self.status.turn_tools = 0;
|
|
||||||
}
|
|
||||||
UiMessage::StatusUpdate(info) => {
|
|
||||||
if !info.dmn_state.is_empty() {
|
|
||||||
self.status.dmn_state = info.dmn_state;
|
|
||||||
self.status.dmn_turns = info.dmn_turns;
|
|
||||||
self.status.dmn_max_turns = info.dmn_max_turns;
|
|
||||||
}
|
|
||||||
if info.prompt_tokens > 0 { self.status.prompt_tokens = info.prompt_tokens; }
|
|
||||||
if !info.model.is_empty() { self.status.model = info.model; }
|
|
||||||
if !info.context_budget.is_empty() { self.status.context_budget = info.context_budget; }
|
|
||||||
}
|
|
||||||
UiMessage::Activity(text) => {
|
|
||||||
if text.is_empty() {
|
|
||||||
self.call_started = None;
|
|
||||||
} else if self.activity.is_empty() || self.call_started.is_none() {
|
|
||||||
self.call_started = Some(std::time::Instant::now());
|
|
||||||
self.call_timeout_secs = crate::config::get().api_stream_timeout_secs;
|
|
||||||
}
|
|
||||||
self.activity = text;
|
|
||||||
}
|
|
||||||
UiMessage::Reasoning(text) => {
|
|
||||||
self.autonomous.current_color = Color::DarkGray;
|
|
||||||
self.autonomous.append_text(&text);
|
|
||||||
}
|
|
||||||
UiMessage::ToolStarted { .. } | UiMessage::ToolFinished { .. } => {}
|
|
||||||
UiMessage::Debug(text) => {
|
|
||||||
self.tools.push_line(format!("[debug] {}", text), Color::DarkGray);
|
|
||||||
}
|
|
||||||
UiMessage::Info(text) => {
|
|
||||||
self.conversation.push_line(text, Color::Cyan);
|
|
||||||
}
|
|
||||||
UiMessage::ContextInfoUpdate(info) => { self.context_info = Some(info); }
|
|
||||||
UiMessage::AgentUpdate(agents) => { self.agent_state = agents; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle global keys only (Ctrl combos, F-keys). Screen-specific
|
|
||||||
/// keys are handled by the active ScreenView's tick method.
|
|
||||||
pub fn handle_key(&mut self, key: KeyEvent) {
|
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('c') => { self.should_quit = true; return; }
|
KeyCode::Char('c') => { self.should_quit = true; return true; }
|
||||||
KeyCode::Char('r') => { self.hotkey_actions.push(HotkeyAction::CycleReasoning); return; }
|
KeyCode::Char('r') => { self.hotkey_actions.push(HotkeyAction::CycleReasoning); return true; }
|
||||||
KeyCode::Char('k') => { self.hotkey_actions.push(HotkeyAction::KillProcess); return; }
|
KeyCode::Char('k') => { self.hotkey_actions.push(HotkeyAction::KillProcess); return true; }
|
||||||
KeyCode::Char('p') => { self.hotkey_actions.push(HotkeyAction::CycleAutonomy); return; }
|
KeyCode::Char('p') => { self.hotkey_actions.push(HotkeyAction::CycleAutonomy); return true; }
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
match key.code {
|
|
||||||
KeyCode::Esc => { self.hotkey_actions.push(HotkeyAction::Interrupt); }
|
|
||||||
KeyCode::Enter if !key.modifiers.contains(KeyModifiers::ALT) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
|
|
||||||
let input: String = self.textarea.lines().join("\n");
|
|
||||||
if !input.is_empty() {
|
|
||||||
if self.input_history.last().map_or(true, |h| h != &input) { self.input_history.push(input.clone()); }
|
|
||||||
self.history_index = None;
|
|
||||||
self.submitted.push(input);
|
|
||||||
self.textarea = new_textarea(vec![String::new()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::CONTROL) => self.scroll_active_up(3),
|
|
||||||
KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => self.scroll_active_down(3),
|
|
||||||
KeyCode::Up => {
|
|
||||||
if !self.input_history.is_empty() {
|
|
||||||
let idx = match self.history_index { None => self.input_history.len() - 1, Some(i) => i.saturating_sub(1) };
|
|
||||||
self.history_index = Some(idx);
|
|
||||||
let mut ta = new_textarea(self.input_history[idx].lines().map(String::from).collect());
|
|
||||||
ta.move_cursor(tui_textarea::CursorMove::End);
|
|
||||||
self.textarea = ta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Down => {
|
|
||||||
if let Some(idx) = self.history_index {
|
|
||||||
if idx + 1 < self.input_history.len() {
|
|
||||||
self.history_index = Some(idx + 1);
|
|
||||||
let mut ta = new_textarea(self.input_history[idx + 1].lines().map(String::from).collect());
|
|
||||||
ta.move_cursor(tui_textarea::CursorMove::End);
|
|
||||||
self.textarea = ta;
|
|
||||||
} else {
|
|
||||||
self.history_index = None;
|
|
||||||
self.textarea = new_textarea(vec![String::new()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::PageUp => self.scroll_active_up(10),
|
|
||||||
KeyCode::PageDown => self.scroll_active_down(10),
|
|
||||||
KeyCode::Tab => {
|
|
||||||
self.active_pane = match self.active_pane {
|
|
||||||
ActivePane::Autonomous => ActivePane::Tools,
|
|
||||||
ActivePane::Tools => ActivePane::Conversation,
|
|
||||||
ActivePane::Conversation => ActivePane::Autonomous,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
_ => { self.textarea.input(key); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_active_up(&mut self, n: u16) {
|
|
||||||
match self.active_pane {
|
|
||||||
ActivePane::Autonomous => self.autonomous.scroll_up(n),
|
|
||||||
ActivePane::Conversation => self.conversation.scroll_up(n),
|
|
||||||
ActivePane::Tools => self.tools.scroll_up(n),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_active_down(&mut self, n: u16) {
|
|
||||||
match self.active_pane {
|
|
||||||
ActivePane::Autonomous => self.autonomous.scroll_down(n),
|
|
||||||
ActivePane::Conversation => self.conversation.scroll_down(n),
|
|
||||||
ActivePane::Tools => self.tools.scroll_down(n),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_resize(&mut self, _width: u16, _height: u16) {}
|
|
||||||
|
|
||||||
pub fn handle_mouse(&mut self, mouse: MouseEvent) {
|
|
||||||
match mouse.kind {
|
|
||||||
MouseEventKind::ScrollUp => self.scroll_active_up(3),
|
|
||||||
MouseEventKind::ScrollDown => self.scroll_active_down(3),
|
|
||||||
MouseEventKind::Down(MouseButton::Left) => {
|
|
||||||
let (x, y) = (mouse.column, mouse.row);
|
|
||||||
for (i, area) in self.pane_areas.iter().enumerate() {
|
|
||||||
if x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height {
|
|
||||||
self.active_pane = match i { 0 => ActivePane::Autonomous, 1 => ActivePane::Conversation, _ => ActivePane::Tools };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw the interact (F1) screen. Overlay screens are drawn
|
|
||||||
/// by the event loop via ScreenView::tick.
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame) {
|
|
||||||
let size = frame.area();
|
|
||||||
self.draw_main(frame, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_channel_status(&mut self, channels: Vec<(String, bool, u32)>) {
|
pub fn set_channel_status(&mut self, channels: Vec<(String, bool, u32)>) {
|
||||||
|
|
@ -539,16 +346,16 @@ impl App {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
pub fn init_terminal() -> io::Result<ratatui::Terminal<CrosstermBackend<io::Stdout>>> {
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
stdout.execute(EnterAlternateScreen)?;
|
stdout.execute(EnterAlternateScreen)?;
|
||||||
stdout.execute(EnableMouseCapture)?;
|
stdout.execute(EnableMouseCapture)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
Terminal::new(backend)
|
ratatui::Terminal::new(backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
pub fn restore_terminal(terminal: &mut ratatui::Terminal<CrosstermBackend<io::Stdout>>) -> io::Result<()> {
|
||||||
terminal::disable_raw_mode()?;
|
terminal::disable_raw_mode()?;
|
||||||
terminal.backend_mut().execute(DisableMouseCapture)?;
|
terminal.backend_mut().execute(DisableMouseCapture)?;
|
||||||
terminal.backend_mut().execute(LeaveAlternateScreen)?;
|
terminal.backend_mut().execute(LeaveAlternateScreen)?;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ impl ScreenView for SubconsciousScreen {
|
||||||
fn label(&self) -> &'static str { "subconscious" }
|
fn label(&self) -> &'static str { "subconscious" }
|
||||||
|
|
||||||
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
|
key: Option<KeyEvent>, app: &mut App) -> Option<ScreenAction> {
|
||||||
// Handle keys
|
// Handle keys
|
||||||
if let Some(key) = key {
|
if let Some(key) = key {
|
||||||
match key.code {
|
match key.code {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ impl ScreenView for ThalamusScreen {
|
||||||
fn label(&self) -> &'static str { "thalamus" }
|
fn label(&self) -> &'static str { "thalamus" }
|
||||||
|
|
||||||
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
key: Option<KeyEvent>, app: &App) -> Option<ScreenAction> {
|
key: Option<KeyEvent>, app: &mut App) -> Option<ScreenAction> {
|
||||||
// Handle keys
|
// Handle keys
|
||||||
if let Some(key) = key {
|
if let Some(key) = key {
|
||||||
match key.code {
|
match key.code {
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ impl ScreenView for UnconsciousScreen {
|
||||||
fn label(&self) -> &'static str { "unconscious" }
|
fn label(&self) -> &'static str { "unconscious" }
|
||||||
|
|
||||||
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
fn tick(&mut self, frame: &mut Frame, area: Rect,
|
||||||
key: Option<KeyEvent>, _app: &App) -> Option<ScreenAction> {
|
key: Option<KeyEvent>, _app: &mut App) -> Option<ScreenAction> {
|
||||||
if let Some(key) = key {
|
if let Some(key) = key {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue