use anyhow::{bail, Result};
use crossterm::event::Event;
use itertools::Itertools;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
Frame,
};
use crate::{
common::{
widget::{
CustomParagraph, CustomStatefulList, CustomStatefulWidget, CustomWidget, LabelSuggestionItem, TextInput,
DEFAULT_HIGHLIGHT_SYMBOL_PREFIX, NEW_LABEL_PREFIX,
},
ExecutionContext, InteractiveProcess,
},
model::LabeledCommand,
storage::SqliteStorage,
Process, ProcessOutput,
};
pub struct LabelProcess<'s> {
storage: &'s SqliteStorage,
command: CustomParagraph<LabeledCommand>,
current_label_ix: usize,
current_label: String,
suggestions: CustomStatefulList<LabelSuggestionItem>,
ctx: ExecutionContext,
}
impl<'s> LabelProcess<'s> {
pub fn new(storage: &'s SqliteStorage, command: LabeledCommand, ctx: ExecutionContext) -> Result<Self> {
let (current_label_ix, current_label) = command
.next_label()
.ok_or_else(|| anyhow::anyhow!("Command doesn't have labels"))?;
let current_label = current_label.to_owned();
let suggestions = Self::suggestion_items_for(storage, &command.root, ¤t_label, TextInput::default())?;
let suggestions = CustomStatefulList::new(suggestions)
.inline(ctx.inline)
.style(Style::default().fg(ctx.theme.main))
.highlight_style(
Style::default()
.bg(ctx.theme.selected_background)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(DEFAULT_HIGHLIGHT_SYMBOL_PREFIX);
let command = CustomParagraph::new(command)
.inline(ctx.inline)
.block_title("Command")
.style(Style::default().fg(ctx.theme.main));
Ok(Self {
storage,
command,
current_label_ix,
current_label,
suggestions,
ctx,
})
}
fn suggestion_items_for(
storage: &SqliteStorage,
root_cmd: &str,
label: &str,
new_suggestion: TextInput,
) -> Result<Vec<LabelSuggestionItem>> {
let mut suggestions = storage
.find_suggestions_for(root_cmd, label)?
.into_iter()
.map(LabelSuggestionItem::Persisted)
.collect_vec();
let mut suggestions_from_label = label
.split('|')
.map(|l| LabelSuggestionItem::Label(l.to_owned()))
.collect_vec();
suggestions.append(&mut suggestions_from_label);
if !new_suggestion.as_str().is_empty() {
suggestions.retain(|s| match s {
LabelSuggestionItem::New(_) => true,
LabelSuggestionItem::Label(l) => l.contains(new_suggestion.as_str()),
LabelSuggestionItem::Persisted(s) => s.suggestion.contains(new_suggestion.as_str()),
})
}
suggestions.insert(0, LabelSuggestionItem::New(new_suggestion));
Ok(suggestions)
}
}
impl<'s> Process for LabelProcess<'s> {
fn min_height(&self) -> usize {
(self.suggestions.len() + 1).clamp(4, 15)
}
fn render<B: Backend>(&mut self, frame: &mut Frame<B>, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(!self.ctx.inline as u16)
.constraints([Constraint::Length(self.command.min_size().height), Constraint::Min(1)])
.split(area);
let header = chunks[0];
let body = chunks[1];
self.command.render_in(frame, header, self.ctx.theme);
self.suggestions.render_in(frame, body, self.ctx.theme);
if let Some(LabelSuggestionItem::New(t)) = self.suggestions.current() {
frame.set_cursor(
body.x
+ DEFAULT_HIGHLIGHT_SYMBOL_PREFIX.len() as u16
+ NEW_LABEL_PREFIX.len() as u16
+ t.cursor().x
+ (!self.ctx.inline as u16),
body.y + (!self.ctx.inline as u16),
);
}
}
fn process_raw_event(&mut self, event: Event) -> Result<Option<ProcessOutput>> {
self.process_event(event)
}
}
impl<'s> InteractiveProcess for LabelProcess<'s> {
fn move_up(&mut self) {
self.suggestions.previous()
}
fn move_down(&mut self) {
self.suggestions.next()
}
fn move_left(&mut self) {
if let Some(LabelSuggestionItem::New(suggestion)) = self.suggestions.current_mut() {
suggestion.move_left()
}
}
fn move_right(&mut self) {
if let Some(LabelSuggestionItem::New(suggestion)) = self.suggestions.current_mut() {
suggestion.move_right()
}
}
fn prev(&mut self) {
self.suggestions.previous()
}
fn next(&mut self) {
self.suggestions.next()
}
fn insert_text(&mut self, text: String) -> Result<()> {
if let Some(LabelSuggestionItem::New(suggestion)) = self.suggestions.current_mut() {
suggestion.insert_text(text);
let suggestion = suggestion.clone();
self.suggestions.update_items(Self::suggestion_items_for(
self.storage,
&self.command.inner().root,
&self.current_label,
suggestion,
)?);
}
Ok(())
}
fn insert_char(&mut self, c: char) -> Result<()> {
if let Some(LabelSuggestionItem::New(suggestion)) = self.suggestions.current_mut() {
suggestion.insert_char(c);
let suggestion = suggestion.clone();
self.suggestions.update_items(Self::suggestion_items_for(
self.storage,
&self.command.inner().root,
&self.current_label,
suggestion,
)?);
}
Ok(())
}
fn delete_char(&mut self, backspace: bool) -> Result<()> {
if let Some(LabelSuggestionItem::New(suggestion)) = self.suggestions.current_mut() {
if suggestion.delete_char(backspace) {
let suggestion = suggestion.clone();
self.suggestions.update_items(Self::suggestion_items_for(
self.storage,
&self.command.inner().root,
&self.current_label,
suggestion,
)?);
}
}
Ok(())
}
fn delete_current(&mut self) -> Result<()> {
if let Some(LabelSuggestionItem::Persisted(_)) = self.suggestions.current() {
if let Some(LabelSuggestionItem::Persisted(suggestion)) = self.suggestions.delete_current() {
self.storage.delete_label_suggestion(&suggestion)?;
}
}
Ok(())
}
fn accept_current(&mut self) -> Result<Option<ProcessOutput>> {
if let Some(suggestion) = self.suggestions.current_mut() {
match suggestion {
LabelSuggestionItem::New(value) => {
if !value.as_str().is_empty() {
let suggestion = self
.command
.inner()
.new_suggestion_for(&self.current_label, value.as_str());
self.storage.insert_label_suggestion(&suggestion)?;
}
self.command.inner_mut().set_next_label(value.as_str());
}
LabelSuggestionItem::Label(value) => {
self.command.inner_mut().set_next_label(value.clone());
}
LabelSuggestionItem::Persisted(suggestion) => {
suggestion.increment_usage();
self.storage.update_label_suggestion(suggestion)?;
self.command.inner_mut().set_next_label(&suggestion.suggestion);
}
}
match self.command.inner().next_label() {
Some((ix, label)) => {
self.current_label_ix = ix;
self.current_label = label.to_owned();
let suggestions = Self::suggestion_items_for(
self.storage,
&self.command.inner().root,
label,
TextInput::default(),
)?;
self.suggestions.update_items(suggestions);
self.suggestions.reset_state();
Ok(None)
}
None => Ok(Some(ProcessOutput::output(self.command.inner().to_string()))),
}
} else {
bail!("Expected at least one suggestion")
}
}
fn exit(&mut self) -> Result<ProcessOutput> {
Ok(ProcessOutput::output(self.command.inner().to_string()))
}
}