use crate::help;
use crate::key::{self, Binding};
use crate::viewport;
use rusty_bubbletea::key::KeyPressMsg;
use rusty_bubbletea::model::{Cmd, Msg};
use rusty_lipgloss::{self, Style};
use rusty_x_ansi;
#[derive(Debug)]
pub struct Model {
pub key_map: KeyMap,
pub help: help::Model,
cols: Vec<Column>,
rows: Vec<Row>,
cursor: usize,
focus: bool,
styles: Styles,
viewport: viewport::Model,
start: usize,
end: usize,
}
pub type Row = Vec<String>;
#[derive(Debug, Clone)]
pub struct Column {
pub title: String,
pub width: usize,
}
#[derive(Debug, Clone)]
pub struct KeyMap {
pub line_up: Binding,
pub line_down: Binding,
pub page_up: Binding,
pub page_down: Binding,
pub half_page_up: Binding,
pub half_page_down: Binding,
pub goto_top: Binding,
pub goto_bottom: Binding,
}
impl KeyMap {
pub fn short_help(&self) -> Vec<Binding> {
vec![self.line_up.clone(), self.line_down.clone()]
}
pub fn full_help(&self) -> Vec<Vec<Binding>> {
vec![
vec![
self.line_up.clone(),
self.line_down.clone(),
self.goto_top.clone(),
self.goto_bottom.clone(),
],
vec![
self.page_up.clone(),
self.page_down.clone(),
self.half_page_up.clone(),
self.half_page_down.clone(),
],
]
}
}
impl help::KeyMap for KeyMap {
fn short_help(&self) -> Vec<Binding> {
KeyMap::short_help(self)
}
fn full_help(&self) -> Vec<Vec<Binding>> {
KeyMap::full_help(self)
}
}
pub fn default_key_map() -> KeyMap {
KeyMap {
line_up: key::new_binding(vec![
key::with_keys(&["up", "k"]),
key::with_help("↑/k", "up"),
]),
line_down: key::new_binding(vec![
key::with_keys(&["down", "j"]),
key::with_help("↓/j", "down"),
]),
page_up: key::new_binding(vec![
key::with_keys(&["b", "pgup"]),
key::with_help("b/pgup", "page up"),
]),
page_down: key::new_binding(vec![
key::with_keys(&["f", "pgdown", "space"]),
key::with_help("f/pgdn", "page down"),
]),
half_page_up: key::new_binding(vec![
key::with_keys(&["u", "ctrl+u"]),
key::with_help("u", "½ page up"),
]),
half_page_down: key::new_binding(vec![
key::with_keys(&["d", "ctrl+d"]),
key::with_help("d", "½ page down"),
]),
goto_top: key::new_binding(vec![
key::with_keys(&["home", "g"]),
key::with_help("g/home", "go to start"),
]),
goto_bottom: key::new_binding(vec![
key::with_keys(&["end", "G"]),
key::with_help("G/end", "go to end"),
]),
}
}
#[derive(Debug, Clone)]
pub struct Styles {
pub header: Style,
pub cell: Style,
pub selected: Style,
}
pub fn default_styles() -> Styles {
Styles {
selected: rusty_lipgloss::new_style().bold(true).foreground("212"),
header: rusty_lipgloss::new_style().bold(true).padding(&[0, 1]),
cell: rusty_lipgloss::new_style().padding(&[0, 1]),
}
}
pub type Option = Box<dyn FnOnce(&mut Model)>;
pub fn new(opts: Vec<Option>) -> Model {
let mut m = Model {
cursor: 0,
viewport: viewport::new(vec![viewport::with_height(20)]),
key_map: default_key_map(),
help: help::new(),
styles: default_styles(),
cols: vec![],
rows: vec![],
focus: false,
start: 0,
end: 0,
};
for opt in opts {
opt(&mut m);
}
m.update_viewport();
m
}
pub fn with_columns(cols: &[Column]) -> Option {
let cols = cols.to_vec();
Box::new(move |m: &mut Model| {
m.cols = cols;
})
}
pub fn with_rows(rows: &[Row]) -> Option {
let rows = rows.to_vec();
Box::new(move |m: &mut Model| {
m.rows = rows;
})
}
pub fn with_height(h: usize) -> Option {
Box::new(move |m: &mut Model| {
let hh = rusty_lipgloss::size::height(&m.headers_view());
m.viewport.set_height(h - hh);
})
}
pub fn with_width(w: usize) -> Option {
Box::new(move |m: &mut Model| {
m.viewport.set_width(w);
})
}
pub fn with_focused(f: bool) -> Option {
Box::new(move |m: &mut Model| {
m.focus = f;
})
}
pub fn with_styles(s: Styles) -> Option {
Box::new(move |m: &mut Model| {
m.styles = s;
})
}
pub fn with_key_map(km: KeyMap) -> Option {
Box::new(move |m: &mut Model| {
m.key_map = km;
})
}
impl Model {
pub fn set_styles(&mut self, s: Styles) {
self.styles = s;
self.update_viewport();
}
pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
if !self.focus {
return None;
}
if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
let k = &m.0;
if key::matches(k, std::slice::from_ref(&self.key_map.line_up)) {
self.move_up(1);
} else if key::matches(k, std::slice::from_ref(&self.key_map.line_down)) {
self.move_down(1);
} else if key::matches(k, std::slice::from_ref(&self.key_map.page_up)) {
self.move_up(self.viewport.height());
} else if key::matches(k, std::slice::from_ref(&self.key_map.page_down)) {
self.move_down(self.viewport.height());
} else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_up)) {
self.move_up(self.viewport.height() / 2);
} else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_down)) {
self.move_down(self.viewport.height() / 2);
} else if key::matches(k, std::slice::from_ref(&self.key_map.goto_top)) {
self.goto_top();
} else if key::matches(k, std::slice::from_ref(&self.key_map.goto_bottom)) {
self.goto_bottom();
}
}
None
}
pub fn focused(&self) -> bool {
self.focus
}
pub fn focus(&mut self) {
self.focus = true;
self.update_viewport();
}
pub fn blur(&mut self) {
self.focus = false;
self.update_viewport();
}
pub fn view(&self) -> String {
self.headers_view() + "\n" + &self.viewport.view()
}
pub fn help_view(&self) -> String {
self.help.view(&self.key_map)
}
pub fn update_viewport(&mut self) {
let mut rendered_rows: Vec<String> = Vec::with_capacity(self.rows.len());
self.start = clamp(
self.cursor.saturating_sub(self.viewport.height()),
0,
self.cursor,
);
self.end = clamp(
self.cursor + self.viewport.height(),
self.cursor,
self.rows.len(),
);
for i in self.start..self.end {
rendered_rows.push(self.render_row(i));
}
let refs: Vec<&str> = rendered_rows.iter().map(|s| s.as_str()).collect();
self.viewport
.set_content(&rusty_lipgloss::join::join_vertical(
rusty_lipgloss::LEFT,
&refs,
));
}
pub fn selected_row(&self) -> std::option::Option<Row> {
if self.cursor >= self.rows.len() {
return None;
}
Some(self.rows[self.cursor].clone())
}
pub fn rows(&self) -> &[Row] {
&self.rows
}
pub fn columns(&self) -> &[Column] {
&self.cols
}
pub fn set_rows(&mut self, r: &[Row]) {
self.rows = r.to_vec();
if self.cursor > self.rows.len().saturating_sub(1) {
self.cursor = self.rows.len().saturating_sub(1);
}
self.update_viewport();
}
pub fn set_columns(&mut self, c: &[Column]) {
self.cols = c.to_vec();
self.update_viewport();
}
pub fn set_width(&mut self, w: usize) {
self.viewport.set_width(w);
self.update_viewport();
}
pub fn set_height(&mut self, h: usize) {
let hh = rusty_lipgloss::size::height(&self.headers_view());
self.viewport.set_height(h - hh);
self.update_viewport();
}
pub fn height(&self) -> usize {
self.viewport.height()
}
pub fn width(&self) -> usize {
self.viewport.width()
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn set_cursor(&mut self, n: usize) {
self.cursor = clamp(n, 0, self.rows.len().saturating_sub(1));
self.update_viewport();
}
pub fn move_up(&mut self, n: usize) {
self.cursor = clamp(
self.cursor.saturating_sub(n),
0,
self.rows.len().saturating_sub(1),
);
let mut offset = self.viewport.y_offset();
if self.start == 0 {
offset = clamp(offset, 0, self.cursor);
} else if self.start < self.viewport.height() {
offset = clamp(clamp(offset + n, 0, self.cursor), 0, self.viewport.height());
} else if offset >= 1 {
offset = clamp(offset + n, 1, self.viewport.height());
}
self.viewport.set_y_offset(offset);
self.update_viewport();
}
pub fn move_down(&mut self, n: usize) {
self.cursor = clamp(self.cursor + n, 0, self.rows.len().saturating_sub(1));
self.update_viewport();
let mut offset = self.viewport.y_offset();
if self.end == self.rows.len() && offset > 0 {
offset = clamp(offset - n, 1, self.viewport.height());
} else if self.cursor > (self.end - self.start) / 2 && offset > 0 {
offset = clamp(offset - n, 1, self.cursor);
} else if offset > 1 {
} else if self.cursor > offset + self.viewport.height() - 1 {
offset = clamp(offset + 1, 0, 1);
}
self.viewport.set_y_offset(offset);
}
pub fn goto_top(&mut self) {
let n = self.cursor;
self.move_up(n);
}
pub fn goto_bottom(&mut self) {
let n = self.rows.len();
self.move_down(n);
}
pub fn from_values(&mut self, value: &str, separator: &str) {
let mut rows: Vec<Row> = vec![];
for line in value.split('\n') {
let mut r: Row = vec![];
for field in line.split(separator) {
r.push(field.to_string());
}
rows.push(r);
}
self.set_rows(&rows);
}
fn headers_view(&self) -> String {
let mut s: Vec<String> = Vec::with_capacity(self.cols.len());
for col in &self.cols {
if col.width == 0 {
continue;
}
let style = rusty_lipgloss::new_style()
.width(col.width)
.max_width(col.width)
.inline(true);
let rendered_cell = style.render(&rusty_x_ansi::truncate(&col.title, col.width, "…"));
s.push(self.styles.header.clone().render(&rendered_cell));
}
let refs: Vec<&str> = s.iter().map(|x| x.as_str()).collect();
rusty_lipgloss::join::join_horizontal(rusty_lipgloss::TOP, &refs)
}
fn render_row(&self, r: usize) -> String {
let mut s: Vec<String> = Vec::with_capacity(self.cols.len());
for (i, value) in self.rows[r].iter().enumerate() {
if self.cols[i].width == 0 {
continue;
}
let style = rusty_lipgloss::new_style()
.width(self.cols[i].width)
.max_width(self.cols[i].width)
.inline(true);
let rendered_cell =
style.render(&rusty_x_ansi::truncate(value, self.cols[i].width, "…"));
s.push(self.styles.cell.clone().render(&rendered_cell));
}
let refs: Vec<&str> = s.iter().map(|x| x.as_str()).collect();
let row = rusty_lipgloss::join::join_horizontal(rusty_lipgloss::TOP, &refs);
if r == self.cursor {
return self.styles.selected.clone().render(&row);
}
row
}
}
fn clamp(v: usize, low: usize, high: usize) -> usize {
v.max(low).min(high)
}