Better switcher behaviour
This commit is contained in:
parent
0bd78521e3
commit
7aafdaa90f
8 changed files with 337 additions and 155 deletions
|
|
@ -11,22 +11,22 @@ pub enum Dir {
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Char(char), // Insert a character
|
Char(char), // Insert a character
|
||||||
Backspace, // Backspace a character
|
Backspace, // Backspace a character
|
||||||
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base)
|
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base)
|
||||||
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 action
|
Cancel, // Cancels the current action
|
||||||
Continue, // Continue past an info-only element (like a help screen)
|
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'
|
||||||
Quit, // Quit the application
|
Quit, // Quit the application
|
||||||
OpenPrompt, // Open the command prompt
|
OpenPrompt, // Open the command prompt
|
||||||
OpenSwitcher, // Open the buffer switcher
|
OpenSwitcher, // Open the buffer switcher
|
||||||
Show(String), // Display some arbitrary text to the user
|
Show(Option<String>, String), // Display an optionally titled informational text box to the user
|
||||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -37,6 +37,12 @@ pub enum Event {
|
||||||
Raw(RawEvent),
|
Raw(RawEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Action> for Event {
|
||||||
|
fn from(action: Action) -> Self {
|
||||||
|
Self::Action(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
pub fn from_raw(e: TerminalEvent) -> Self {
|
pub fn from_raw(e: TerminalEvent) -> Self {
|
||||||
Self::Raw(RawEvent(e))
|
Self::Raw(RawEvent(e))
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ fn main() -> Result<(), Error> {
|
||||||
// Have the UI handle events
|
// Have the UI handle events
|
||||||
if ui
|
if ui
|
||||||
.handle(&mut state, Event::from_raw(ev))
|
.handle(&mut state, Event::from_raw(ev))
|
||||||
.map_or(false, |r| r.should_end())
|
.map_or(false, |r| r.into_ended().is_some())
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ impl Default for BorderTheme {
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub ui_bg: Color,
|
pub ui_bg: Color,
|
||||||
pub select_bg: Color,
|
pub select_bg: Color,
|
||||||
|
pub unfocus_select_bg: Color,
|
||||||
pub margin_bg: Color,
|
pub margin_bg: Color,
|
||||||
pub margin_line_num: Color,
|
pub margin_line_num: Color,
|
||||||
pub border: BorderTheme,
|
pub border: BorderTheme,
|
||||||
|
|
@ -48,6 +49,7 @@ impl Default for Theme {
|
||||||
Self {
|
Self {
|
||||||
ui_bg: Color::AnsiValue(235),
|
ui_bg: Color::AnsiValue(235),
|
||||||
select_bg: Color::AnsiValue(23),
|
select_bg: Color::AnsiValue(23),
|
||||||
|
unfocus_select_bg: Color::AnsiValue(240),
|
||||||
margin_bg: Color::Reset,
|
margin_bg: Color::Reset,
|
||||||
margin_line_num: Color::AnsiValue(245),
|
margin_line_num: Color::AnsiValue(245),
|
||||||
border: BorderTheme::default(),
|
border: BorderTheme::default(),
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ impl Element for Doc {
|
||||||
};
|
};
|
||||||
|
|
||||||
match event.to_action(|e| e.to_open_switcher()) {
|
match event.to_action(|e| e.to_open_switcher()) {
|
||||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))),
|
||||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||||
self.buffer = new_buffer;
|
self.buffer = new_buffer;
|
||||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||||
|
|
|
||||||
|
|
@ -152,10 +152,12 @@ impl Input {
|
||||||
c => (state.theme.text, c),
|
c => (state.theme.text, c),
|
||||||
};
|
};
|
||||||
frame
|
frame
|
||||||
.with_bg(if selected {
|
.with_bg(if !selected {
|
||||||
|
Color::Reset
|
||||||
|
} else if frame.has_focus() {
|
||||||
state.theme.select_bg
|
state.theme.select_bg
|
||||||
} else {
|
} else {
|
||||||
Color::Reset
|
state.theme.unfocus_select_bg
|
||||||
})
|
})
|
||||||
.with_fg(fg)
|
.with_fg(fg)
|
||||||
.text([i as isize, 0], &[c]);
|
.text([i as isize, 0], &[c]);
|
||||||
|
|
|
||||||
159
src/ui/mod.rs
159
src/ui/mod.rs
|
|
@ -20,42 +20,51 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum CannotEnd {}
|
pub enum CannotEnd {}
|
||||||
pub struct CanEnd;
|
|
||||||
|
|
||||||
pub struct Resp<CanEnd = CannotEnd> {
|
pub struct Resp<End = CannotEnd> {
|
||||||
should_end: Option<CanEnd>,
|
ended: Option<End>,
|
||||||
pub action: Option<Action>,
|
pub event: Option<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Resp<CanEnd> {
|
impl Resp<CannotEnd> {
|
||||||
pub fn end(action: impl Into<Option<Action>>) -> Self {
|
pub fn into_can_end<End>(self) -> Resp<End> {
|
||||||
Self {
|
|
||||||
should_end: Some(CanEnd),
|
|
||||||
action: action.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn should_end(&self) -> bool {
|
|
||||||
self.should_end.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Resp<T> {
|
|
||||||
pub fn handled(action: impl Into<Option<Action>>) -> Self {
|
|
||||||
Self {
|
|
||||||
should_end: None,
|
|
||||||
action: action.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_can_end(self) -> Resp<CanEnd> {
|
|
||||||
Resp {
|
Resp {
|
||||||
should_end: None,
|
ended: None,
|
||||||
action: self.action,
|
event: self.event,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<End> Resp<End> {
|
||||||
|
pub fn end(event: Option<Event>) -> Self
|
||||||
|
where
|
||||||
|
End: Default,
|
||||||
|
{
|
||||||
|
Self::end_with(Default::default(), event)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_with(end: End, event: Option<Event>) -> Self {
|
||||||
|
Self {
|
||||||
|
ended: Some(end),
|
||||||
|
event: event.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handled(event: Option<Event>) -> Self {
|
||||||
|
Self {
|
||||||
|
ended: None,
|
||||||
|
event: event.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_end(&self) -> bool {
|
||||||
|
self.ended.is_some()
|
||||||
|
}
|
||||||
|
pub fn into_ended(mut self) -> Option<End> {
|
||||||
|
self.ended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Element<CanEnd = CannotEnd> {
|
pub trait Element<CanEnd = CannotEnd> {
|
||||||
/// Attempt to handle an event.
|
/// Attempt to handle an event.
|
||||||
///
|
///
|
||||||
|
|
@ -77,12 +86,104 @@ impl std::ops::Deref for Label {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Label {
|
||||||
|
pub fn requested_height(&self) -> usize {
|
||||||
|
self.0.lines().count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Visual for Label {
|
impl Visual for Label {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| {
|
frame.with(|frame| {
|
||||||
for (idx, line) in self.lines().enumerate() {
|
for (idx, line) in self.lines().enumerate() {
|
||||||
frame.text([0, idx as isize], line.chars());
|
frame.text([0, idx as isize], line.chars());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List selection
|
||||||
|
pub struct Options<T> {
|
||||||
|
pub selected: usize,
|
||||||
|
// (score, option)
|
||||||
|
pub options: Vec<T>,
|
||||||
|
pub ranking: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Options<T> {
|
||||||
|
pub fn new(options: impl IntoIterator<Item = T>) -> Self {
|
||||||
|
let (ranking, options) = options.into_iter().enumerate().unzip();
|
||||||
|
Self {
|
||||||
|
selected: 0,
|
||||||
|
options,
|
||||||
|
ranking,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_scoring<F: FnMut(&T) -> Option<u32>>(&mut self, mut f: F) {
|
||||||
|
let mut ranking = self
|
||||||
|
.options
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, o)| Some((i, f(o)?)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ranking.sort_by_key(|(_, score)| *score);
|
||||||
|
self.ranking = ranking.into_iter().map(|(i, _)| i).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requested_height(&self) -> usize {
|
||||||
|
2 + self.ranking.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Element<T> for Options<T> {
|
||||||
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> {
|
||||||
|
match event.to_action(|e| e.to_go().or_else(|| e.to_move())) {
|
||||||
|
Some(Action::Move(Dir::Up, false, _)) => {
|
||||||
|
self.selected = (self.selected + self.ranking.len() - 1) % self.ranking.len();
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
|
Some(Action::Move(Dir::Down, false, _)) => {
|
||||||
|
self.selected = (self.selected + 1) % self.ranking.len();
|
||||||
|
Ok(Resp::handled(None))
|
||||||
|
}
|
||||||
|
Some(Action::Go) => {
|
||||||
|
if self.selected < self.ranking.len() {
|
||||||
|
Ok(Resp::end_with(
|
||||||
|
self.options.remove(self.ranking[self.selected]),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Visual> Visual for Options<T> {
|
||||||
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
let mut frame = frame.with_border(
|
||||||
|
if frame.has_focus() {
|
||||||
|
&state.theme.focus_border
|
||||||
|
} else {
|
||||||
|
&state.theme.border
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, idx) in self.ranking.iter().enumerate() {
|
||||||
|
let option = &mut self.options[*idx];
|
||||||
|
frame
|
||||||
|
.rect([0, i], [frame.size()[0], 1])
|
||||||
|
.with_bg(if self.selected == i {
|
||||||
|
state.theme.select_bg
|
||||||
|
} else {
|
||||||
|
Color::Reset
|
||||||
|
})
|
||||||
|
.fill(' ')
|
||||||
|
.with(|f| option.render(state, f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
200
src/ui/prompt.rs
200
src/ui/prompt.rs
|
|
@ -21,39 +21,48 @@ impl Prompt {
|
||||||
match self.buffer.text.to_string().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(
|
||||||
"{} {}",
|
Some(format!("Version")),
|
||||||
env!("CARGO_PKG_NAME"),
|
format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
|
||||||
env!("CARGO_PKG_VERSION")
|
)),
|
||||||
))),
|
"?" | "help" => Some(Action::Show(
|
||||||
"?" | "help" => Some(Action::Show(format!(
|
Some(format!("Help")),
|
||||||
"Temporary help info:\n\
|
format!(
|
||||||
|
"Temporary help info:\n\
|
||||||
- quit\n\
|
- quit\n\
|
||||||
- version\n\
|
- version\n\
|
||||||
- pane_move_left\n\
|
- pane_move_left\n\
|
||||||
- pane_move_right\n\
|
- pane_move_right\n\
|
||||||
- help"
|
- help"
|
||||||
))),
|
),
|
||||||
|
)),
|
||||||
"pane_move_left" => Some(Action::PaneMove(Dir::Left)),
|
"pane_move_left" => Some(Action::PaneMove(Dir::Left)),
|
||||||
"pane_move_right" => Some(Action::PaneMove(Dir::Right)),
|
"pane_move_right" => Some(Action::PaneMove(Dir::Right)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn requested_height(&self) -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element<CanEnd> for Prompt {
|
impl Element<()> for Prompt {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||||
match event.to_action(|e| e.to_go().or_else(|| e.to_cancel())) {
|
match event.to_action(|e| e.to_go().or_else(|| e.to_cancel())) {
|
||||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||||
Some(Action::Go) => {
|
Some(Action::Go) => {
|
||||||
if let Some(action) = self.get_action() {
|
if let Some(action) = self.get_action() {
|
||||||
self.buffer.clear();
|
self.buffer.clear();
|
||||||
Ok(Resp::handled(action))
|
Ok(Resp::end(Some(action.into())))
|
||||||
} else {
|
} else {
|
||||||
Ok(Resp::handled(Action::Show(format!(
|
Ok(Resp::handled(Some(
|
||||||
"unknown command `{}`",
|
Action::Show(
|
||||||
self.buffer.text.to_string()
|
Some(format!("Error")),
|
||||||
))))
|
format!("unknown command `{}`", self.buffer.text.to_string()),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => self
|
_ => self
|
||||||
|
|
@ -71,30 +80,44 @@ impl Visual for Prompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Show {
|
pub struct Show {
|
||||||
|
pub title: Option<String>,
|
||||||
pub label: Label,
|
pub label: Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element<CanEnd> for Show {
|
impl Show {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
pub fn requested_height(&self) -> usize {
|
||||||
|
self.label.requested_height() + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element<()> for Show {
|
||||||
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||||
match event.to_action(|e| {
|
match event.to_action(|e| {
|
||||||
e.to_cancel()
|
e.to_cancel()
|
||||||
.or_else(|| e.to_continue())
|
.or_else(|| e.to_continue())
|
||||||
.or_else(|| e.to_char().map(Action::Char))
|
.or_else(|| e.to_char().map(Action::Char))
|
||||||
}) {
|
}) {
|
||||||
// Shows cannot be cancelled, so pass the cancel along to the parent task
|
// Shows cannot be cancelled, so pass the cancel along to the parent task
|
||||||
Some(Action::Cancel) => Ok(Resp::end(Some(Action::Cancel))),
|
Some(Action::Cancel) => Ok(Resp::end(Some(Action::Cancel.into()))),
|
||||||
// A continue ends the show
|
// A continue ends the show
|
||||||
Some(Action::Continue) => Ok(Resp::end(None)),
|
Some(Action::Continue) => Ok(Resp::end(None)),
|
||||||
// Pass attempts to type to the parent prompt task
|
// All other events end the show and get passed to the parent
|
||||||
// TODO: Don't assume that a `Show` is always the child of the prompt
|
_ => Ok(Resp::end(Some(event))),
|
||||||
Some(Action::Char(c)) => Ok(Resp::end(Some(Action::Char(c)))),
|
|
||||||
_ => Ok(Resp::handled(None)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Show {
|
impl Visual for Show {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
let mut frame = frame.with_border(
|
||||||
|
if frame.has_focus() {
|
||||||
|
&state.theme.focus_border
|
||||||
|
} else {
|
||||||
|
&state.theme.border
|
||||||
|
},
|
||||||
|
self.title.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
let lines = self.label.lines().count();
|
let lines = self.label.lines().count();
|
||||||
self.label.render(
|
self.label.render(
|
||||||
state,
|
state,
|
||||||
|
|
@ -111,10 +134,16 @@ pub struct Confirm {
|
||||||
pub action: Action,
|
pub action: Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element<CanEnd> for Confirm {
|
impl Confirm {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
pub fn requested_height(&self) -> usize {
|
||||||
|
self.label.requested_height() + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element<()> for Confirm {
|
||||||
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||||
match event.to_action(|e| e.to_yes().or_else(|| e.to_no()).or_else(|| e.to_cancel())) {
|
match event.to_action(|e| e.to_yes().or_else(|| e.to_no()).or_else(|| e.to_cancel())) {
|
||||||
Some(Action::Yes) => Ok(Resp::end(Some(self.action.clone()))),
|
Some(Action::Yes) => Ok(Resp::end(Some(self.action.clone().into()))),
|
||||||
Some(Action::No | Action::Cancel) => Ok(Resp::end(None)),
|
Some(Action::No | Action::Cancel) => Ok(Resp::end(None)),
|
||||||
// All other events get swallowed
|
// All other events get swallowed
|
||||||
_ => Ok(Resp::handled(None)),
|
_ => Ok(Resp::handled(None)),
|
||||||
|
|
@ -124,6 +153,15 @@ impl Element<CanEnd> for Confirm {
|
||||||
|
|
||||||
impl Visual for Confirm {
|
impl Visual for Confirm {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
|
let mut frame = frame.with_border(
|
||||||
|
if frame.has_focus() {
|
||||||
|
&state.theme.focus_border
|
||||||
|
} else {
|
||||||
|
&state.theme.border
|
||||||
|
},
|
||||||
|
Some("Question"),
|
||||||
|
);
|
||||||
|
|
||||||
let lines = self.label.lines().count();
|
let lines = self.label.lines().count();
|
||||||
self.label.render(
|
self.label.render(
|
||||||
state,
|
state,
|
||||||
|
|
@ -136,60 +174,80 @@ impl Visual for Confirm {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Switcher {
|
pub struct Switcher {
|
||||||
pub selected: usize,
|
pub options: Options<BufferId>,
|
||||||
pub options: Vec<BufferId>,
|
// Filter
|
||||||
|
pub buffer: Buffer,
|
||||||
|
pub cursor_id: CursorId,
|
||||||
|
pub input: Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element<CanEnd> for Switcher {
|
impl Switcher {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
pub fn new(buffers: impl IntoIterator<Item = BufferId>) -> Self {
|
||||||
match event.to_action(|e| e.to_cancel().or_else(|| e.to_go()).or_else(|| e.to_move())) {
|
let mut buffer = Buffer::default();
|
||||||
Some(Action::Move(Dir::Up, false, _)) => {
|
Self {
|
||||||
self.selected = (self.selected + self.options.len() - 1) % self.options.len();
|
options: Options::new(buffers),
|
||||||
Ok(Resp::handled(None))
|
cursor_id: buffer.start_session(),
|
||||||
}
|
buffer,
|
||||||
Some(Action::Move(Dir::Down, false, _)) => {
|
input: Input::prompt(),
|
||||||
self.selected = (self.selected + 1) % self.options.len();
|
}
|
||||||
Ok(Resp::handled(None))
|
}
|
||||||
}
|
|
||||||
Some(Action::Go) => Ok(Resp::end(
|
pub fn requested_height(&self) -> usize {
|
||||||
if let Some(buffer) = self.options.get(self.selected) {
|
self.options.requested_height() + 3
|
||||||
Some(Action::SwitchBuffer(*buffer))
|
}
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
},
|
impl Element<()> for Switcher {
|
||||||
)),
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||||
|
match event.to_action(|e| e.to_cancel()) {
|
||||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||||
// All other events get swallowed
|
_ => match self.options.handle(state, event).map(Resp::into_ended) {
|
||||||
_ => Ok(Resp::handled(None)),
|
Ok(Some(buffer_id)) => Ok(Resp::end(Some(Action::SwitchBuffer(buffer_id).into()))),
|
||||||
|
Ok(None) => Ok(Resp::handled(None)),
|
||||||
|
Err(event) => {
|
||||||
|
let res = self
|
||||||
|
.input
|
||||||
|
.handle(&mut self.buffer, self.cursor_id, event)
|
||||||
|
.map(Resp::into_can_end);
|
||||||
|
// Score entries
|
||||||
|
let filter = self.buffer.text.to_string();
|
||||||
|
self.options.apply_scoring(|b| {
|
||||||
|
let Some(buffer) = state.buffers.get(*b) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
match buffer.path.as_ref() {
|
||||||
|
Some(path) if path.display().to_string().contains(&filter) => Some(1),
|
||||||
|
Some(_) => None,
|
||||||
|
None => Some(0),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Switcher {
|
impl Visual for Switcher {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
for (i, buffer) in self.options.iter().enumerate() {
|
frame
|
||||||
let Some(buffer) = state.buffers.get(*buffer) else {
|
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
|
||||||
continue;
|
.with(|f| self.options.render(state, f));
|
||||||
};
|
frame
|
||||||
let buffer_name = match &buffer.path {
|
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||||
Some(path) => path.display().to_string(),
|
.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||||
None => format!("<Untitled>"),
|
}
|
||||||
};
|
}
|
||||||
frame
|
|
||||||
.rect(
|
impl Visual for BufferId {
|
||||||
[
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
0,
|
let Some(buffer) = state.buffers.get(*self) else {
|
||||||
frame.size()[1].saturating_sub(3 + self.options.len()) + i,
|
return;
|
||||||
],
|
};
|
||||||
[frame.size()[0], 1],
|
let buffer_name = match &buffer.path {
|
||||||
)
|
Some(path) => path.display().to_string(),
|
||||||
.with_bg(if self.selected == i {
|
None => format!("<Untitled>"),
|
||||||
state.theme.select_bg
|
};
|
||||||
} else {
|
frame.text([0, 0], buffer_name.chars());
|
||||||
state.theme.ui_bg
|
|
||||||
})
|
|
||||||
.fill(' ')
|
|
||||||
.text([0, 0], buffer_name.chars());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,17 @@ pub enum Task {
|
||||||
Switcher(Switcher),
|
Switcher(Switcher),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Task {
|
||||||
|
pub fn requested_height(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Prompt(p) => p.requested_height(),
|
||||||
|
Self::Show(s) => s.requested_height(),
|
||||||
|
Self::Confirm(c) => c.requested_height(),
|
||||||
|
Self::Switcher(s) => s.requested_height(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Root {
|
impl Root {
|
||||||
pub fn new(state: &mut State, buffers: &[BufferId]) -> Self {
|
pub fn new(state: &mut State, buffers: &[BufferId]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -24,8 +35,8 @@ impl Root {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element<CanEnd> for Root {
|
impl Element<()> for Root {
|
||||||
fn handle(&mut self, state: &mut State, mut event: Event) -> Result<Resp<CanEnd>, Event> {
|
fn handle(&mut self, state: &mut State, mut event: Event) -> Result<Resp<()>, Event> {
|
||||||
// Pass the event down through the list of tasks until we meet one that can handle it
|
// Pass the event down through the list of tasks until we meet one that can handle it
|
||||||
let mut task_idx = self.tasks.len();
|
let mut task_idx = self.tasks.len();
|
||||||
let action = loop {
|
let action = loop {
|
||||||
|
|
@ -33,10 +44,8 @@ impl Element<CanEnd> for Root {
|
||||||
Some(task_idx) => task_idx,
|
Some(task_idx) => task_idx,
|
||||||
None => {
|
None => {
|
||||||
break match self.panes.handle(state, event) {
|
break match self.panes.handle(state, event) {
|
||||||
Ok(resp) => resp.action,
|
Ok(resp) => resp.event,
|
||||||
Err(event) => {
|
Err(event) => Some(event),
|
||||||
event.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel()))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -51,21 +60,23 @@ impl Element<CanEnd> for Root {
|
||||||
match res {
|
match res {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
// If the task has requested that it should end, kill it and all of its children
|
// If the task has requested that it should end, kill it and all of its children
|
||||||
if resp.should_end() {
|
if resp.is_end() {
|
||||||
self.tasks.truncate(task_idx);
|
self.tasks.truncate(task_idx);
|
||||||
}
|
}
|
||||||
if let Some(action) = resp.action {
|
event = if let Some(event) = resp.event {
|
||||||
event = Event::Action(action);
|
event
|
||||||
} else {
|
} else {
|
||||||
break None;
|
break None;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
Err(e) => event = e,
|
Err(e) => event = e,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle 'top-level' actions
|
// Handle 'top-level' actions
|
||||||
if let Some(action) = action {
|
if let Some(action) =
|
||||||
|
action.and_then(|e| e.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel())))
|
||||||
|
{
|
||||||
match action {
|
match action {
|
||||||
Action::OpenPrompt => {
|
Action::OpenPrompt => {
|
||||||
self.tasks.clear(); // Prompt overrides all
|
self.tasks.clear(); // Prompt overrides all
|
||||||
|
|
@ -73,16 +84,17 @@ impl Element<CanEnd> for Root {
|
||||||
}
|
}
|
||||||
Action::OpenSwitcher => {
|
Action::OpenSwitcher => {
|
||||||
self.tasks.clear(); // Prompt overrides all
|
self.tasks.clear(); // Prompt overrides all
|
||||||
self.tasks.push(Task::Switcher(Switcher {
|
self.tasks
|
||||||
selected: 0,
|
.push(Task::Switcher(Switcher::new(state.buffers.keys())));
|
||||||
options: state.buffers.keys().collect(),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
|
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
|
||||||
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
|
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
|
||||||
action: Action::Quit,
|
action: Action::Quit,
|
||||||
})),
|
})),
|
||||||
Action::Show(text) => self.tasks.push(Task::Show(Show { label: Label(text) })),
|
Action::Show(title, text) => self.tasks.push(Task::Show(Show {
|
||||||
|
title,
|
||||||
|
label: Label(text),
|
||||||
|
})),
|
||||||
Action::Quit => return Ok(Resp::end(None)),
|
Action::Quit => return Ok(Resp::end(None)),
|
||||||
action => {
|
action => {
|
||||||
return self
|
return self
|
||||||
|
|
@ -102,35 +114,36 @@ impl Visual for Root {
|
||||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.fill(' ');
|
frame.fill(' ');
|
||||||
|
|
||||||
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
let task_has_focus = !self.tasks.is_empty();
|
||||||
|
|
||||||
// Display status bar
|
// Determine how much space the active task should use
|
||||||
let status_size = if let Some(Task::Prompt(p)) = self.tasks.first_mut() {
|
let task_h = self.tasks.last().map_or(0, |t| t.requested_height());
|
||||||
|
|
||||||
|
// Render active task
|
||||||
|
if let Some(task) = self.tasks.last_mut() {
|
||||||
frame
|
frame
|
||||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
.rect(
|
||||||
.with(|frame| p.render(state, frame));
|
[0, frame.size()[1].saturating_sub(task_h)],
|
||||||
3
|
[frame.size()[0], task_h],
|
||||||
} else {
|
)
|
||||||
0
|
.with_focus(task_has_focus)
|
||||||
};
|
.with(|frame| match task {
|
||||||
|
Task::Prompt(p) => p.render(state, frame),
|
||||||
|
Task::Show(s) => s.render(state, frame),
|
||||||
|
Task::Confirm(c) => c.render(state, frame),
|
||||||
|
Task::Switcher(s) => s.render(state, frame),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render panes
|
||||||
frame
|
frame
|
||||||
.rect(
|
.rect(
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[frame.size()[0], frame.size()[1].saturating_sub(status_size)],
|
[frame.size()[0], frame.size()[1].saturating_sub(task_h)],
|
||||||
)
|
)
|
||||||
.with_focus(!task_has_focus)
|
.with_focus(!task_has_focus)
|
||||||
.with(|frame| {
|
.with(|frame| {
|
||||||
self.panes.render(state, frame);
|
self.panes.render(state, frame);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(task) = self.tasks.last_mut() {
|
|
||||||
match task {
|
|
||||||
Task::Prompt(_) => {} // Prompt isn't rendered, it's always rendered above
|
|
||||||
Task::Show(s) => s.render(state, frame),
|
|
||||||
Task::Confirm(c) => c.render(state, frame),
|
|
||||||
Task::Switcher(s) => s.render(state, frame),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue