use rusty_bubbles::help;
use rusty_bubbles::key::{self, Binding};
use rusty_bubbles::textarea;
use rusty_bubbletea::cursor::Cursor;
use rusty_bubbletea::model::Model as ModelTrait;
use rusty_bubbletea::screen::WindowSizeMsg;
use rusty_bubbletea::{batch, quit, Cmd, KeyPressMsg, Msg, Program, View};
use rusty_lipgloss::border;
use rusty_lipgloss::join::join_horizontal;
use rusty_lipgloss::{Color, Style, TOP};
const INITIAL_INPUTS: usize = 2;
const MAX_INPUTS: usize = 6;
const MIN_INPUTS: usize = 1;
const HELP_HEIGHT: usize = 5;
fn cursor_color() -> Color {
Color::parse("212")
}
fn cursor_line_style() -> Style {
Style::new()
.background_color(Color::parse("57"))
.foreground_color(Color::parse("230"))
}
fn placeholder_style() -> Style {
Style::new().foreground_color(Color::parse("238"))
}
fn focused_placeholder_style() -> Style {
Style::new().foreground_color(Color::parse("99"))
}
fn focused_border_style() -> Style {
Style::new()
.border(border::rounded_border(), &[true, true, true, true])
.border_foreground(&["238"])
}
fn blurred_border_style() -> Style {
Style::new().border(border::hidden_border(), &[true, true, true, true])
}
fn end_of_buffer_style() -> Style {
Style::new().foreground_color(Color::parse("235"))
}
fn new_textarea() -> textarea::Model {
let mut t = textarea::new();
t.prompt = String::new();
t.placeholder = "Type something".to_string();
t.show_line_numbers = true;
t.set_virtual_cursor(true);
let mut s = t.styles().clone();
s.cursor.color = cursor_color();
s.focused.placeholder = focused_placeholder_style();
s.blurred.placeholder = placeholder_style();
s.focused.cursor_line = cursor_line_style();
s.focused.cursor_line_number = cursor_line_style();
s.focused.base = focused_border_style();
s.blurred.base = blurred_border_style();
s.focused.end_of_buffer = end_of_buffer_style();
s.blurred.end_of_buffer = end_of_buffer_style();
t.set_styles(s);
t.key_map.delete_word_backward.set_enabled(false);
t.key_map.line_next = key::new_binding(vec![key::with_keys(&["down"])]);
t.key_map.line_previous = key::new_binding(vec![key::with_keys(&["up"])]);
t.blur();
t
}
struct Keymap {
next: Binding,
prev: Binding,
add: Binding,
remove: Binding,
quit: Binding,
}
struct Model {
width: usize,
height: usize,
keymap: Keymap,
help: help::Model,
inputs: Vec<textarea::Model>,
focus: usize,
}
impl Model {
fn new_model() -> Self {
let mut m = Model {
width: 0,
height: 0,
keymap: Keymap {
next: key::new_binding(vec![
key::with_keys(&["tab"]),
key::with_help("tab", "next"),
]),
prev: key::new_binding(vec![
key::with_keys(&["shift+tab"]),
key::with_help("shift+tab", "prev"),
]),
add: key::new_binding(vec![
key::with_keys(&["ctrl+n"]),
key::with_help("ctrl+n", "add an editor"),
]),
remove: key::new_binding(vec![
key::with_keys(&["ctrl+w"]),
key::with_help("ctrl+w", "remove an editor"),
]),
quit: key::new_binding(vec![
key::with_keys(&["esc", "ctrl+c"]),
key::with_help("esc", "quit"),
]),
},
help: help::new(),
inputs: vec![],
focus: 0,
};
for _ in 0..INITIAL_INPUTS {
m.inputs.push(new_textarea());
}
m.inputs[m.focus].focus();
m.update_keybindings();
m
}
fn size_inputs(&mut self) {
let n = self.inputs.len();
let w = self.width / n;
let h = self.height.saturating_sub(HELP_HEIGHT);
for i in 0..n {
self.inputs[i].set_width(w);
self.inputs[i].set_height(h);
}
}
fn update_keybindings(&mut self) {
self.keymap.add.set_enabled(self.inputs.len() < MAX_INPUTS);
self.keymap
.remove
.set_enabled(self.inputs.len() > MIN_INPUTS);
}
fn input_views(&self) -> Vec<String> {
let mut views = Vec::new();
for i in 0..self.inputs.len() {
views.push(self.inputs[i].view());
}
views
}
fn cursor(&self) -> Option<Cursor> {
let focused_input = &self.inputs[self.focus];
if focused_input.virtual_cursor() {
return None;
}
let views = self.input_views();
let mut c = focused_input.cursor()?;
for v in views.iter().take(self.focus) {
c.position.x += rusty_lipgloss::size::width(v);
}
Some(c)
}
}
impl ModelTrait for Model {
fn init(&self) -> Cmd {
Some(Box::new(|| Some(textarea::blink())))
}
fn update(&mut self, msg: &dyn Msg) -> Cmd {
let mut cmds: Vec<Cmd> = Vec::new();
if let Some(k) = msg.as_any().downcast_ref::<KeyPressMsg>() {
let s = k.0.to_string();
if key::matches(&s, std::slice::from_ref(&self.keymap.quit)) {
for i in 0..self.inputs.len() {
self.inputs[i].blur();
}
return quit();
} else if key::matches(&s, std::slice::from_ref(&self.keymap.next)) {
self.inputs[self.focus].blur();
self.focus += 1;
if self.focus > self.inputs.len() - 1 {
self.focus = 0;
}
cmds.push(self.inputs[self.focus].focus());
} else if key::matches(&s, std::slice::from_ref(&self.keymap.prev)) {
self.inputs[self.focus].blur();
if self.focus == 0 {
self.focus = self.inputs.len() - 1;
} else {
self.focus -= 1;
}
cmds.push(self.inputs[self.focus].focus());
} else if key::matches(&s, std::slice::from_ref(&self.keymap.add)) {
self.inputs.push(new_textarea());
} else if key::matches(&s, std::slice::from_ref(&self.keymap.remove)) {
self.inputs.pop();
if self.focus > self.inputs.len() - 1 {
self.focus = self.inputs.len() - 1;
}
}
}
if let Some(w) = msg.as_any().downcast_ref::<WindowSizeMsg>() {
self.height = w.height;
self.width = w.width;
}
self.update_keybindings();
self.size_inputs();
for i in 0..self.inputs.len() {
cmds.push(self.inputs[i].update(msg));
}
batch(cmds)
}
fn view(&self) -> View {
let help_view = self.help.short_help_view(&[
self.keymap.next.clone(),
self.keymap.prev.clone(),
self.keymap.add.clone(),
self.keymap.remove.clone(),
self.keymap.quit.clone(),
]);
let views = self.input_views();
let joined = join_horizontal(
TOP,
&views.iter().map(|s| s.as_str()).collect::<Vec<&str>>(),
);
let mut v = View::new(&(joined + "\n\n" + &help_view));
v.alt_screen = true;
v.cursor = self.cursor();
v
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let p = Program::new(Model::new_model());
p.run()?;
Ok(())
}