Unify prompt and doc input

This commit is contained in:
Joshua Barretto 2025-06-08 22:08:36 +01:00
parent 84056b5a74
commit 67dac456af
9 changed files with 337 additions and 292 deletions

View file

@ -17,7 +17,8 @@ pub enum Action {
PaneMove(Dir), // Move panes PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane PaneClose, // Close the current pane
Cancel, // Cancels the current context Cancel, // Cancels the current action
Continue, // Continue past an info-only element (like a help screen)
Go, // Search, accept, or select the current option Go, // Search, accept, or select the current option
Yes, // A binary confirmation is answered 'yes' Yes, // A binary confirmation is answered 'yes'
No, // A binary confirmation is answered 'no' No, // A binary confirmation is answered 'no'
@ -234,6 +235,22 @@ impl RawEvent {
} }
} }
pub fn to_continue(&self) -> Option<Action> {
if matches!(
&self.0,
TerminalEvent::Key(KeyEvent {
code: KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
..
})
) {
Some(Action::Continue)
} else {
None
}
}
pub fn to_no(&self) -> Option<Action> { pub fn to_no(&self) -> Option<Action> {
if matches!(self.to_char(), Some('n' | 'N')) { if matches!(self.to_char(), Some('n' | 'N')) {
Some(Action::No) Some(Action::No)

View file

@ -1,8 +1,4 @@
use crate::{ use crate::{Args, Dir, Error, theme};
Action, Args, Color, Dir, Error, Event, theme,
ui::{self, Element as _, Resp},
};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use slotmap::{HopSlotMap, new_key_type}; use slotmap::{HopSlotMap, new_key_type};
use std::{io, ops::Range, path::PathBuf}; use std::{io, ops::Range, path::PathBuf};
@ -33,10 +29,17 @@ impl Cursor {
} }
} }
#[derive(Default)]
pub struct Text { pub struct Text {
chars: Vec<char>, chars: Vec<char>,
} }
impl ToString for Text {
fn to_string(&self) -> String {
self.chars.iter().copied().collect()
}
}
impl Text { impl Text {
pub fn to_coord(&self, pos: usize) -> [isize; 2] { pub fn to_coord(&self, pos: usize) -> [isize; 2] {
let mut n = 0; let mut n = 0;
@ -53,7 +56,7 @@ impl Text {
[(pos - last_n) as isize, i.saturating_sub(1) as isize] [(pos - last_n) as isize, i.saturating_sub(1) as isize]
} }
pub fn to_pos(&self, mut coord: [isize; 2]) -> usize { pub fn to_pos(&self, coord: [isize; 2]) -> usize {
if coord[1] < 0 { if coord[1] < 0 {
return 0; return 0;
} }
@ -98,14 +101,15 @@ impl Text {
} }
} }
#[derive(Default)]
pub struct Buffer { pub struct Buffer {
pub path: PathBuf, pub path: Option<PathBuf>,
pub text: Text, pub text: Text,
pub cursors: HopSlotMap<CursorId, Cursor>, pub cursors: HopSlotMap<CursorId, Cursor>,
} }
impl Buffer { impl Buffer {
pub fn new(path: PathBuf) -> Result<Self, Error> { pub fn from_file(path: PathBuf) -> Result<Self, Error> {
let chars = match std::fs::read_to_string(&path) { let chars = match std::fs::read_to_string(&path) {
Ok(s) => s.chars().collect(), Ok(s) => s.chars().collect(),
// If the file doesn't exist, create a new file // If the file doesn't exist, create a new file
@ -113,7 +117,7 @@ impl Buffer {
Err(err) => return Err(err.into()), Err(err) => return Err(err.into()),
}; };
Ok(Self { Ok(Self {
path, path: Some(path),
text: Text { chars }, text: Text { chars },
cursors: HopSlotMap::default(), cursors: HopSlotMap::default(),
}) })
@ -139,7 +143,7 @@ impl Buffer {
cursor.reset_desired_col(&self.text); cursor.reset_desired_col(&self.text);
} }
Dir::Up => { Dir::Up => {
let mut coord = self.text.to_coord(cursor.pos); let coord = self.text.to_coord(cursor.pos);
// Special case: pressing 'up' at the top of the screen resets the cursor to the beginning // Special case: pressing 'up' at the top of the screen resets the cursor to the beginning
if coord[1] <= 0 { if coord[1] <= 0 {
cursor.pos = 0; cursor.pos = 0;
@ -151,7 +155,7 @@ impl Buffer {
} }
} }
Dir::Down => { Dir::Down => {
let mut coord = self.text.to_coord(cursor.pos); let coord = self.text.to_coord(cursor.pos);
cursor.pos = self cursor.pos = self
.text .text
.to_pos([cursor.desired_col, coord[1] + dist[1] as isize]); .to_pos([cursor.desired_col, coord[1] + dist[1] as isize]);
@ -258,7 +262,7 @@ impl TryFrom<Args> for State {
}; };
for path in args.paths { for path in args.paths {
this.buffers.insert(Buffer::new(path)?); this.buffers.insert(Buffer::from_file(path)?);
} }
Ok(this) Ok(this)

85
src/ui/doc.rs Normal file
View file

@ -0,0 +1,85 @@
use super::*;
use crate::{
state::{BufferId, CursorId},
terminal::CursorStyle,
};
use std::collections::HashMap;
#[derive(Clone)]
pub struct Doc {
buffer: BufferId,
// Remember the cursor we use for each buffer
cursors: HashMap<BufferId, CursorId>,
input: Input,
}
impl Doc {
pub fn new(state: &mut State, buffer: BufferId) -> Self {
Self {
buffer,
// TODO: Don't index directly
cursors: [(buffer, state.buffers[buffer].start_session())]
.into_iter()
.collect(),
input: Input::default(),
}
}
pub fn title(&self, state: &State) -> Option<String> {
let Some(buffer) = state.buffers.get(self.buffer) else {
return None;
};
Some(buffer.path.as_ref()?.display().to_string())
}
pub fn close(self, state: &mut State) {
for (buffer, cursor) in self.cursors {
let Some(buffer) = state.buffers.get_mut(buffer) else {
continue;
};
buffer.end_session(cursor);
}
}
}
impl Element for Doc {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event);
};
match event.to_action(|e| e.to_open_switcher()) {
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
Some(Action::SwitchBuffer(new_buffer)) => {
self.buffer = new_buffer;
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event);
};
// Start a new cursor session for this buffer if one doesn't exist
let cursor_id = *self
.cursors
.entry(self.buffer)
.or_insert_with(|| buffer.start_session());
self.input.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
_ => {
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event);
};
let cursor_id = self.cursors[&self.buffer];
self.input.handle(buffer, cursor_id, event)
}
}
}
}
impl Visual for Doc {
fn render(&mut self, state: &State, frame: &mut Rect) {
let Some(buffer) = state.buffers.get(self.buffer) else {
return;
};
let cursor_id = self.cursors[&self.buffer];
self.input.render(state, buffer, cursor_id, frame);
}
}

View file

@ -1,59 +1,193 @@
use super::*; use super::*;
use crate::terminal::CursorStyle; use crate::{
state::{Buffer, CursorId},
terminal::CursorStyle,
};
#[derive(Default)] #[derive(Copy, Clone, Default)]
enum Mode {
#[default]
Doc,
Prompt,
}
#[derive(Clone, Default)]
pub struct Input { pub struct Input {
pub text: Vec<char>, pub mode: Mode,
pub cursor: usize, // x/y location in the buffer that the pane is trying to focus on
pub preamble: &'static str, pub focus: [isize; 2],
// Remember the last known size for things like scrolling
pub last_size: [usize; 2],
} }
impl Input { impl Input {
pub fn get_text(&self) -> String { pub fn prompt() -> Self {
self.text.iter().copied().collect() Self {
mode: Mode::Prompt,
..Self::default()
}
} }
}
impl Element for Input { pub fn refocus(&mut self, buffer: &mut Buffer, cursor_id: CursorId) {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> { let Some(cursor) = buffer.cursors.get(cursor_id) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
for i in 0..2 {
self.focus[i] = self.focus[i].clamp(
cursor_coord[i] - self.last_size[i] as isize + 1,
cursor_coord[i],
);
}
}
pub fn handle(
&mut self,
buffer: &mut Buffer,
cursor_id: CursorId,
event: Event,
) -> Result<Resp, Event> {
match event.to_action(|e| e.to_char().map(Action::Char).or_else(|| e.to_move())) { match event.to_action(|e| e.to_char().map(Action::Char).or_else(|| e.to_move())) {
Some(Action::Char('\x08')) => {
self.cursor = self.cursor.saturating_sub(1);
if self.text.len() > self.cursor {
self.text.remove(self.cursor);
}
Ok(Resp::handled(None))
}
Some(Action::Char(c)) => { Some(Action::Char(c)) => {
self.text.insert(self.cursor, c); if c == '\x08' {
self.cursor += 1; buffer.backspace(cursor_id);
} else if c == '\x7F' {
buffer.delete(cursor_id);
} else {
buffer.enter(cursor_id, c);
}
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Move(Dir::Left, _, _)) => { Some(Action::Move(dir, page, retain_base)) => {
self.cursor = self.cursor.saturating_sub(1); let dist = if page {
Ok(Resp::handled(None)) self.last_size.map(|s| s.saturating_sub(3).max(1))
} } else {
Some(Action::Move(Dir::Right, _, _)) => { [1, 1]
self.cursor = (self.cursor + 1).min(self.text.len()); };
buffer.move_cursor(cursor_id, dir, dist, retain_base);
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
_ => Err(event), _ => Err(event),
} }
} }
}
impl Visual for Input { pub fn render(
fn render(&mut self, state: &State, frame: &mut Rect) { &mut self,
frame.with(|frame| { state: &State,
frame.fill(' '); buffer: &Buffer,
frame.text([0, 0], self.preamble.chars()); cursor_id: CursorId,
frame: &mut Rect,
) {
let title = if let Some(path) = &buffer.path {
Some(path.display().to_string())
} else {
None
};
frame // Add frame
.rect([self.preamble.chars().count(), 0], frame.size()) let mut frame = frame.with_border(
.with(|frame| { if frame.has_focus() {
frame.text([0, 0], &self.text); &state.theme.focus_border
frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar); } else {
}); &state.theme.border
}); },
title.as_deref(),
);
let Some(cursor) = buffer.cursors.get(cursor_id) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
let margin_w = match self.mode {
Mode::Prompt => 2,
Mode::Doc => line_num_w + 2,
};
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]];
let mut pos = 0;
for (i, (line_num, (line_pos, line))) in buffer
.text
.lines()
.map(move |line| {
let line_pos = pos;
pos += line.len();
(line_pos, line)
})
.enumerate()
.skip(self.focus[1].max(0) as usize)
.enumerate()
.take(frame.size()[1])
{
// Margin
match self.mode {
Mode::Prompt => frame
.rect([0, i], [1, 1])
.with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([0, 0], ">".chars()),
Mode::Doc => frame
.rect([0, i], [margin_w, 1])
.with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars()),
};
// Line
{
let mut frame = frame.rect([margin_w, i], [!0, 1]);
for i in 0..frame.size()[0] {
let coord = self.focus[0] + i as isize;
if (0..line.len() as isize).contains(&coord) {
let pos = line_pos + coord as usize;
let selected = cursor.selection().map_or(false, |s| s.contains(&pos));
let (fg, c) = match line[coord as usize] {
'\n' if selected => (state.theme.whitespace, '⮠'),
c => (state.theme.text, c),
};
frame
.with_bg(if selected {
state.theme.select_bg
} else {
Color::Reset
})
.with_fg(fg)
.text([i as isize, 0], &[c]);
}
}
// Set cursor position
if cursor_coord[1] == line_num as isize {
frame.set_cursor(
[cursor_coord[0] - self.focus[0], 0],
CursorStyle::BlinkingBar,
);
}
}
pos += line.len();
}
} }
} }
// impl Visual for Input {
// fn render(&mut self, state: &State, frame: &mut Rect) {
// frame.with(|frame| {
// frame.fill(' ');
// frame.text([0, 0], self.preamble.chars());
// frame
// .rect([self.preamble.chars().count(), 0], frame.size())
// .with(|frame| {
// frame.text([0, 0], &self.text);
// frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
// });
// });
// }
// }

View file

@ -1,3 +1,4 @@
mod doc;
mod input; mod input;
mod panes; mod panes;
mod prompt; mod prompt;
@ -5,6 +6,7 @@ mod root;
mod status; mod status;
pub use self::{ pub use self::{
doc::Doc,
input::Input, input::Input,
panes::Panes, panes::Panes,
prompt::{Confirm, Prompt, Show, Switcher}, prompt::{Confirm, Prompt, Show, Switcher},

View file

@ -1,186 +1,5 @@
use super::*; use super::*;
use crate::{ use crate::state::BufferId;
state::{Buffer, BufferId, Cursor, CursorId},
terminal::CursorStyle,
};
use std::collections::HashMap;
#[derive(Clone)]
pub struct Doc {
buffer: BufferId,
// Remember the cursor we use for each buffer
cursors: HashMap<BufferId, CursorId>,
// x/y location in the buffer that the pane is trying to focus on
focus: [isize; 2],
// Remember the last known size for things like scrolling
last_size: [usize; 2],
}
impl Doc {
pub fn new(state: &mut State, buffer: BufferId) -> Self {
Self {
buffer,
// TODO: Don't index directly
cursors: [(buffer, state.buffers[buffer].start_session())]
.into_iter()
.collect(),
focus: [0, 0],
last_size: [1, 1],
}
}
fn refocus(&mut self, state: &mut State) {
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return;
};
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
for i in 0..2 {
self.focus[i] = self.focus[i].clamp(
cursor_coord[i] - self.last_size[i] as isize + 1,
cursor_coord[i],
);
}
}
pub fn close(self, state: &mut State) {
for (buffer, cursor) in self.cursors {
let Some(buffer) = state.buffers.get_mut(buffer) else {
continue;
};
buffer.end_session(cursor);
}
}
}
impl Element for Doc {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event);
};
match event.to_action(|e| {
e.to_char()
.map(Action::Char)
.or_else(|| e.to_move())
.or_else(|| e.to_pane_move().map(Action::PaneMove))
.or_else(|| e.to_open_switcher())
}) {
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
Some(Action::SwitchBuffer(new_buffer)) => {
self.buffer = new_buffer;
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
return Err(event);
};
// Start a new cursor session for this buffer if one doesn't exist
self.cursors
.entry(self.buffer)
.or_insert_with(|| buffer.start_session());
self.refocus(state);
Ok(Resp::handled(None))
}
Some(Action::Char(c)) => {
let cursor_id = self.cursors[&self.buffer];
if c == '\x08' {
buffer.backspace(cursor_id);
} else if c == '\x7F' {
buffer.delete(cursor_id);
} else {
buffer.enter(cursor_id, c);
}
self.refocus(state);
Ok(Resp::handled(None))
}
Some(Action::Move(dir, page, retain_base)) => {
let dist = if page {
self.last_size.map(|s| s.saturating_sub(3).max(1))
} else {
[1, 1]
};
buffer.move_cursor(self.cursors[&self.buffer], dir, dist, retain_base);
self.refocus(state);
Ok(Resp::handled(None))
}
_ => Err(event),
}
}
}
impl Visual for Doc {
fn render(&mut self, state: &State, frame: &mut Rect) {
let Some(buffer) = state.buffers.get(self.buffer) else {
return;
};
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
let margin_w = line_num_w + 2;
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]];
let mut pos = 0;
for (i, (line_num, (line_pos, line))) in buffer
.text
.lines()
.map(move |line| {
let line_pos = pos;
pos += line.len();
(line_pos, line)
})
.enumerate()
.skip(self.focus[1].max(0) as usize)
.enumerate()
.take(frame.size()[1])
{
// Margin
frame
.rect([0, i], [margin_w, 1])
.with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([1, 0], format!("{:>line_num_w$}", line_num + 1).chars());
// Line
{
let mut frame = frame.rect([margin_w, i], [!0, 1]);
for i in 0..frame.size()[0] {
let coord = self.focus[0] + i as isize;
if (0..line.len() as isize).contains(&coord) {
let pos = line_pos + coord as usize;
let selected = cursor.selection().map_or(false, |s| s.contains(&pos));
let (fg, c) = match line[coord as usize] {
'\n' if selected => (state.theme.whitespace, '⮠'),
c => (state.theme.text, c),
};
frame
.with_bg(if selected {
state.theme.select_bg
} else {
Color::Reset
})
.with_fg(fg)
.text([i as isize, 0], &[c]);
}
}
// Set cursor position
if cursor_coord[1] == line_num as isize {
frame.set_cursor(
[cursor_coord[0] - self.focus[0], 0],
CursorStyle::BlinkingBar,
);
}
}
pos += line.len();
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub enum Pane { pub enum Pane {
@ -188,20 +7,6 @@ pub enum Pane {
Doc(Doc), Doc(Doc),
} }
impl Pane {
fn title(&self, state: &State) -> Option<String> {
match self {
Self::Empty => None,
Self::Doc(doc) => {
let Some(buffer) = state.buffers.get(doc.buffer) else {
return None;
};
Some(buffer.path.display().to_string())
}
}
}
}
pub struct Panes { pub struct Panes {
selected: usize, selected: usize,
panes: Vec<Pane>, panes: Vec<Pane>,
@ -262,7 +67,7 @@ impl Element for Panes {
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
// Pass anything else through to the active pane // Pass anything else through to the active pane
err => { _ => {
if let Some(pane) = self.panes.get_mut(self.selected) { if let Some(pane) = self.panes.get_mut(self.selected) {
// Pass to pane // Pass to pane
match pane { match pane {
@ -287,18 +92,10 @@ impl Visual for Panes {
for (i, pane) in self.panes.iter_mut().enumerate() { for (i, pane) in self.panes.iter_mut().enumerate() {
let (x0, x1) = (boundary(i), boundary(i + 1)); let (x0, x1) = (boundary(i), boundary(i + 1));
let is_selected = self.selected == i;
let border_theme = if frame.has_focus() && is_selected {
&state.theme.focus_border
} else {
&state.theme.border
};
// Draw pane contents // Draw pane contents
frame frame
.rect([x0, 0], [x1 - x0, frame.size()[1]]) .rect([x0, 0], [x1 - x0, frame.size()[1]])
.with_border(border_theme, pane.title(state).as_deref()) .with_focus(self.selected == i)
.with_focus(is_selected)
.with(|frame| match pane { .with(|frame| match pane {
Pane::Empty => {} Pane::Empty => {}
Pane::Doc(doc) => doc.render(state, frame), Pane::Doc(doc) => doc.render(state, frame),

View file

@ -1,14 +1,24 @@
use super::*; use super::*;
use crate::state::BufferId; use crate::state::{Buffer, BufferId, CursorId};
use std::str::FromStr;
pub struct Prompt { pub struct Prompt {
pub input: Input, buffer: Buffer,
cursor_id: CursorId,
input: Input,
} }
impl Prompt { impl Prompt {
pub fn new() -> Self {
let mut buffer = Buffer::default();
Self {
cursor_id: buffer.start_session(),
buffer,
input: Input::prompt(),
}
}
pub fn get_action(&self) -> Option<Action> { pub fn get_action(&self) -> Option<Action> {
match self.input.get_text().as_str() { match self.buffer.text.to_string().as_str() {
// The root sees 'cancel' as an initiator for quitting // The root sees 'cancel' as an initiator for quitting
"q" | "quit" => Some(Action::Cancel), "q" | "quit" => Some(Action::Cancel),
"version" => Some(Action::Show(format!( "version" => Some(Action::Show(format!(
@ -41,18 +51,21 @@ impl Element<CanEnd> for Prompt {
} else { } else {
Ok(Resp::end(Action::Show(format!( Ok(Resp::end(Action::Show(format!(
"unknown command `{}`", "unknown command `{}`",
self.input.get_text() self.buffer.text.to_string()
)))) ))))
} }
} }
_ => self.input.handle(state, event).map(Resp::into_can_end), _ => self
.input
.handle(&mut self.buffer, self.cursor_id, event)
.map(Resp::into_can_end),
} }
} }
} }
impl Visual for Prompt { impl Visual for Prompt {
fn render(&mut self, state: &State, frame: &mut Rect) { fn render(&mut self, state: &State, frame: &mut Rect) {
frame.with(|f| self.input.render(state, f)); frame.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
} }
} }
@ -62,9 +75,9 @@ pub struct Show {
impl Element<CanEnd> for Show { impl Element<CanEnd> for Show {
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> { fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
match event.to_action(|e| e.to_cancel()) { match event.to_action(|e| e.to_continue()) {
Some(Action::Cancel) => Ok(Resp::end(None)), Some(Action::Continue) => Ok(Resp::end(None)),
_ => Err(event), _ => Ok(Resp::handled(None)),
} }
} }
} }
@ -147,6 +160,10 @@ impl Visual for Switcher {
let Some(buffer) = state.buffers.get(*buffer) else { let Some(buffer) = state.buffers.get(*buffer) else {
continue; continue;
}; };
let buffer_name = match &buffer.path {
Some(path) => path.display().to_string(),
None => format!("<Untitled>"),
};
frame frame
.rect( .rect(
[ [
@ -161,7 +178,7 @@ impl Visual for Switcher {
state.theme.ui_bg state.theme.ui_bg
}) })
.fill(' ') .fill(' ')
.text([0, 0], buffer.path.display().to_string().chars()); .text([0, 0], buffer_name.chars());
} }
} }
} }

View file

@ -65,12 +65,7 @@ impl Element<CanEnd> for Root {
match action { match action {
Action::OpenPrompt => { Action::OpenPrompt => {
self.tasks.clear(); // Prompt overrides all self.tasks.clear(); // Prompt overrides all
self.tasks.push(Task::Prompt(Prompt { self.tasks.push(Task::Prompt(Prompt::new()));
input: Input {
preamble: "> ",
..Input::default()
},
}));
} }
Action::OpenSwitcher => { Action::OpenSwitcher => {
self.tasks.clear(); // Prompt overrides all self.tasks.clear(); // Prompt overrides all
@ -106,24 +101,20 @@ impl Visual for Root {
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_))); let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
// Display status bar // Display status bar
frame let status_size = if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3]) frame
.with_border( .rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
if task_has_focus { .with(|frame| p.render(state, frame));
&state.theme.focus_border 3
} else { } else {
&state.theme.border 0
}, };
Some("Prompt (press alt + enter)"),
)
.with(|frame| {
if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
p.render(state, frame);
}
});
frame frame
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)]) .rect(
[0, 0],
[frame.size()[0], frame.size()[1].saturating_sub(status_size)],
)
.with_focus(!task_has_focus) .with_focus(!task_has_focus)
.with(|frame| { .with(|frame| {
self.panes.render(state, frame); self.panes.render(state, frame);

View file

@ -1,3 +1 @@
use super::*;
pub struct Status; pub struct Status;