meli/jobs_view: add column headers and sorting
Sort with `sort <index> [asc/desc]` command or by pressing `1..5` keys. Press them again to toggle between asc and desc. Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>pull/284/head
parent
f93adb683a
commit
f193bdf685
|
@ -361,7 +361,7 @@ define_commands!([
|
|||
)
|
||||
},
|
||||
{ tags: ["go"],
|
||||
desc: "go [n], switch to nth mailbox in this account",
|
||||
desc: "go <n>, switch to nth mailbox in this account",
|
||||
tokens: &[One(Literal("goto")), One(MailboxIndexValue)],
|
||||
parser: (
|
||||
fn goto(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
|
@ -397,7 +397,27 @@ define_commands!([
|
|||
let (input, _) = eof(input)?;
|
||||
Ok((input, (Sort(p.0, p.1))))
|
||||
}
|
||||
)},
|
||||
)
|
||||
},
|
||||
{ tags: ["sort"],
|
||||
desc: "sort <column index> [asc/desc], sorts table columns.",
|
||||
tokens: &[One(Literal("sort")), One(IndexValue), ZeroOrOne(Alternatives(&[to_stream!(One(Literal("asc"))), to_stream!(One(Literal("desc")))])) ],
|
||||
parser: (
|
||||
fn sort_column(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("sort")(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, i) = usize_c(input)?;
|
||||
let (input, order) = if input.trim().is_empty() {
|
||||
(input, SortOrder::Desc)
|
||||
} else {
|
||||
let (input, (_, order)) = pair(is_a(" "), sortorder)(input)?;
|
||||
(input, order)
|
||||
};
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, (SortColumn(i, order))))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["set", "set plain", "set threaded", "set compact"],
|
||||
desc: "set [plain/threaded/compact/conversations], changes the mail listing view",
|
||||
tokens: &[One(Literal("set")), One(Alternatives(&[to_stream!(One(Literal("plain"))), to_stream!(One(Literal("threaded"))), to_stream!(One(Literal("compact"))), to_stream!(One(Literal("conversations")))]))],
|
||||
|
@ -997,6 +1017,7 @@ pub fn parse_command(input: &[u8]) -> Result<Action, Error> {
|
|||
goto,
|
||||
listing_action,
|
||||
sort,
|
||||
sort_column,
|
||||
subsort,
|
||||
close,
|
||||
mailinglist,
|
||||
|
|
|
@ -114,6 +114,7 @@ pub enum Action {
|
|||
Listing(ListingAction),
|
||||
ViewMailbox(usize),
|
||||
Sort(SortField, SortOrder),
|
||||
SortColumn(usize, SortOrder),
|
||||
SubSort(SortField, SortOrder),
|
||||
Tab(TabAction),
|
||||
MailingListAction(MailingListAction),
|
||||
|
|
|
@ -26,15 +26,39 @@ use indexmap::IndexMap;
|
|||
use super::*;
|
||||
use crate::{
|
||||
jobs::{JobId, JobMetadata},
|
||||
melib::utils::datetime::{self, formats::RFC3339_DATETIME_AND_SPACE},
|
||||
melib::{
|
||||
utils::datetime::{self, formats::RFC3339_DATETIME_AND_SPACE},
|
||||
SortOrder,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[repr(u8)]
|
||||
enum Column {
|
||||
_0 = 0,
|
||||
_1,
|
||||
_2,
|
||||
_3,
|
||||
_4,
|
||||
}
|
||||
|
||||
const fn _assert_len() {
|
||||
if JobManager::HEADERS.len() != Column::_4 as usize + 1 {
|
||||
panic!("JobManager::HEADERS length changed, please update Column enum accordingly.");
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = _assert_len();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JobManager {
|
||||
cursor_pos: usize,
|
||||
new_cursor_pos: usize,
|
||||
length: usize,
|
||||
data_columns: DataColumns<5>,
|
||||
min_width: [usize; 5],
|
||||
sort_col: Column,
|
||||
sort_order: SortOrder,
|
||||
entries: IndexMap<JobId, JobMetadata>,
|
||||
|
||||
initialized: bool,
|
||||
|
@ -54,8 +78,18 @@ impl std::fmt::Display for JobManager {
|
|||
}
|
||||
|
||||
impl JobManager {
|
||||
const HEADERS: [&str; 5] = ["id", "desc", "started", "finished", "succeeded"];
|
||||
|
||||
pub fn new(context: &Context) -> Self {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
let highlight_theme = if context.settings.terminal.use_color() {
|
||||
crate::conf::value(context, "highlight")
|
||||
} else {
|
||||
ThemeAttribute {
|
||||
attrs: Attr::REVERSE,
|
||||
..ThemeAttribute::default()
|
||||
}
|
||||
};
|
||||
let mut data_columns = DataColumns::default();
|
||||
data_columns.theme_config.set_single_theme(theme_default);
|
||||
Self {
|
||||
|
@ -64,8 +98,11 @@ impl JobManager {
|
|||
entries: IndexMap::default(),
|
||||
length: 0,
|
||||
data_columns,
|
||||
min_width: [0; 5],
|
||||
sort_col: Column::_2,
|
||||
sort_order: SortOrder::Desc,
|
||||
theme_default,
|
||||
highlight_theme: crate::conf::value(context, "highlight"),
|
||||
highlight_theme,
|
||||
initialized: false,
|
||||
dirty: true,
|
||||
movement: None,
|
||||
|
@ -74,43 +111,63 @@ impl JobManager {
|
|||
}
|
||||
|
||||
fn initialize(&mut self, context: &mut Context) {
|
||||
self.entries = (*context.main_loop_handler.job_executor.jobs.lock().unwrap()).clone();
|
||||
self.length = self.entries.len();
|
||||
self.entries.sort_by(|_, a, _, b| a.started.cmp(&b.started));
|
||||
|
||||
self.set_dirty(true);
|
||||
let mut min_width = (
|
||||
"id".len(),
|
||||
"desc".len(),
|
||||
"started".len(),
|
||||
"finished".len(),
|
||||
"succeeded".len(),
|
||||
0,
|
||||
);
|
||||
|
||||
let mut entries = (*context.main_loop_handler.job_executor.jobs.lock().unwrap()).clone();
|
||||
|
||||
self.length = entries.len();
|
||||
entries.sort_by(|_, a, _, b| match (self.sort_col, self.sort_order) {
|
||||
(Column::_0, SortOrder::Asc) => a.id.cmp(&b.id),
|
||||
(Column::_0, SortOrder::Desc) => b.id.cmp(&b.id),
|
||||
(Column::_1, SortOrder::Asc) => a.desc.cmp(&b.desc),
|
||||
(Column::_1, SortOrder::Desc) => b.desc.cmp(&a.desc),
|
||||
(Column::_2, SortOrder::Asc) => a.started.cmp(&b.started),
|
||||
(Column::_2, SortOrder::Desc) => b.started.cmp(&a.started),
|
||||
(Column::_3, SortOrder::Asc) => a.finished.cmp(&b.finished),
|
||||
(Column::_3, SortOrder::Desc) => b.finished.cmp(&a.finished),
|
||||
(Column::_4, SortOrder::Asc) if a.finished.is_some() && b.finished.is_some() => {
|
||||
a.succeeded.cmp(&b.succeeded)
|
||||
}
|
||||
(Column::_4, SortOrder::Desc) if a.finished.is_some() && b.finished.is_some() => {
|
||||
b.succeeded.cmp(&a.succeeded)
|
||||
}
|
||||
(Column::_4, SortOrder::Asc) if a.finished.is_none() => std::cmp::Ordering::Less,
|
||||
(Column::_4, SortOrder::Asc) => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) if a.finished.is_none() => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) => std::cmp::Ordering::Less,
|
||||
});
|
||||
self.entries = entries;
|
||||
|
||||
macro_rules! hdr {
|
||||
($idx:literal) => {{
|
||||
Self::HEADERS[$idx].len() + if self.sort_col as u8 == $idx { 1 } else { 0 }
|
||||
}};
|
||||
}
|
||||
self.min_width = [hdr!(0), hdr!(1), hdr!(2), hdr!(3), hdr!(4)];
|
||||
|
||||
for c in self.entries.values() {
|
||||
/* title */
|
||||
min_width.0 = cmp::max(min_width.0, c.id.to_string().len());
|
||||
self.min_width[0] = cmp::max(self.min_width[0], c.id.to_string().len());
|
||||
/* desc */
|
||||
min_width.1 = cmp::max(min_width.1, c.desc.len());
|
||||
self.min_width[1] = cmp::max(self.min_width[1], c.desc.len());
|
||||
}
|
||||
min_width.2 = "1970-01-01 00:00:00".len();
|
||||
min_width.3 = min_width.2;
|
||||
self.min_width[2] = "1970-01-01 00:00:00".len();
|
||||
self.min_width[3] = self.min_width[2];
|
||||
|
||||
/* name column */
|
||||
self.data_columns.columns[0] =
|
||||
CellBuffer::new_with_context(min_width.0, self.length, None, context);
|
||||
CellBuffer::new_with_context(self.min_width[0], self.length, None, context);
|
||||
/* path column */
|
||||
self.data_columns.columns[1] =
|
||||
CellBuffer::new_with_context(min_width.1, self.length, None, context);
|
||||
CellBuffer::new_with_context(self.min_width[1], self.length, None, context);
|
||||
/* size column */
|
||||
self.data_columns.columns[2] =
|
||||
CellBuffer::new_with_context(min_width.2, self.length, None, context);
|
||||
CellBuffer::new_with_context(self.min_width[2], self.length, None, context);
|
||||
/* subscribed column */
|
||||
self.data_columns.columns[3] =
|
||||
CellBuffer::new_with_context(min_width.3, self.length, None, context);
|
||||
CellBuffer::new_with_context(self.min_width[3], self.length, None, context);
|
||||
self.data_columns.columns[4] =
|
||||
CellBuffer::new_with_context(min_width.4, self.length, None, context);
|
||||
CellBuffer::new_with_context(self.min_width[4], self.length, None, context);
|
||||
|
||||
for (idx, e) in self.entries.values().enumerate() {
|
||||
write_string_to_grid(
|
||||
|
@ -119,7 +176,7 @@ impl JobManager {
|
|||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.0, idx)),
|
||||
((0, idx), (self.min_width[0], idx)),
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -129,7 +186,7 @@ impl JobManager {
|
|||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.1, idx)),
|
||||
((0, idx), (self.min_width[1], idx)),
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -139,7 +196,7 @@ impl JobManager {
|
|||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.2, idx)),
|
||||
((0, idx), (self.min_width[2], idx)),
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -157,7 +214,7 @@ impl JobManager {
|
|||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.3, idx)),
|
||||
((0, idx), (self.min_width[3], idx)),
|
||||
None,
|
||||
);
|
||||
|
||||
|
@ -171,13 +228,13 @@ impl JobManager {
|
|||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
((0, idx), (min_width.4, idx)),
|
||||
((0, idx), (self.min_width[4], idx)),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
if self.length == 0 {
|
||||
let message = "No mailboxes.".to_string();
|
||||
let message = "No jobs.".to_string();
|
||||
self.data_columns.columns[0] =
|
||||
CellBuffer::new_with_context(message.len(), self.length, None, context);
|
||||
write_string_to_grid(
|
||||
|
@ -286,7 +343,7 @@ impl JobManager {
|
|||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
change_colors(grid, new_area, row_attr.fg, row_attr.bg);
|
||||
change_theme(grid, new_area, row_attr);
|
||||
context.dirty_areas.push_back(new_area);
|
||||
}
|
||||
return;
|
||||
|
@ -307,11 +364,10 @@ impl JobManager {
|
|||
.draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area));
|
||||
|
||||
/* highlight cursor */
|
||||
change_colors(
|
||||
change_theme(
|
||||
grid,
|
||||
nth_row_area(area, self.cursor_pos % rows),
|
||||
self.highlight_theme.fg,
|
||||
self.highlight_theme.bg,
|
||||
self.highlight_theme,
|
||||
);
|
||||
|
||||
/* clear gap if available height is more than count of entries */
|
||||
|
@ -337,8 +393,49 @@ impl Component for JobManager {
|
|||
if !self.initialized {
|
||||
self.initialize(context);
|
||||
}
|
||||
{
|
||||
// Draw column headers.
|
||||
let area = nth_row_area(area, 0);
|
||||
clear_area(grid, area, self.theme_default);
|
||||
let mut x_offset = 0;
|
||||
let (upper_left, bottom_right) = area;
|
||||
for (i, (h, w)) in Self::HEADERS.iter().zip(self.min_width).enumerate() {
|
||||
write_string_to_grid(
|
||||
h,
|
||||
grid,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::BOLD,
|
||||
(pos_inc(upper_left, (x_offset, 0)), bottom_right),
|
||||
None,
|
||||
);
|
||||
if self.sort_col as usize == i {
|
||||
use SortOrder::*;
|
||||
let arrow = match (grid.ascii_drawing, self.sort_order) {
|
||||
(true, Asc) => DataColumns::<5>::ARROW_UP_ASCII,
|
||||
(true, Desc) => DataColumns::<5>::ARROW_DOWN_ASCII,
|
||||
(false, Asc) => DataColumns::<5>::ARROW_UP,
|
||||
(false, Desc) => DataColumns::<5>::ARROW_DOWN,
|
||||
};
|
||||
write_string_to_grid(
|
||||
arrow,
|
||||
grid,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
(pos_inc(upper_left, (x_offset + h.len(), 0)), bottom_right),
|
||||
None,
|
||||
);
|
||||
}
|
||||
x_offset += w + 2;
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
self.draw_list(grid, area, context);
|
||||
// Draw entry rows.
|
||||
if let Some(area) = skip_rows(area, 1) {
|
||||
self.draw_list(grid, area, context);
|
||||
}
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
|
@ -358,6 +455,55 @@ impl Component for JobManager {
|
|||
self.set_dirty(true);
|
||||
return false;
|
||||
}
|
||||
UIEvent::Action(Action::SortColumn(column, order)) => {
|
||||
let column = match *column {
|
||||
0 => Column::_0,
|
||||
1 => Column::_1,
|
||||
2 => Column::_2,
|
||||
3 => Column::_3,
|
||||
4 => Column::_4,
|
||||
other => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Invalid column index `{}`: there are {} columns.",
|
||||
other,
|
||||
Self::HEADERS.len()
|
||||
)),
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (self.sort_col, self.sort_order) != (column, *order) {
|
||||
self.sort_col = column;
|
||||
self.sort_order = *order;
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Char(ref c)) if c.is_ascii_digit() => {
|
||||
let n = *c as u8 - b'0'; // safe cast because of is_ascii_digit() check;
|
||||
let column = match n {
|
||||
1 => Column::_0,
|
||||
2 => Column::_1,
|
||||
3 => Column::_2,
|
||||
4 => Column::_3,
|
||||
5 => Column::_4,
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if self.sort_col == column {
|
||||
self.sort_order = !self.sort_order;
|
||||
} else {
|
||||
self.sort_col = column;
|
||||
self.sort_order = SortOrder::default();
|
||||
}
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) =>
|
||||
{
|
||||
|
@ -405,6 +551,9 @@ impl Component for JobManager {
|
|||
self.movement = Some(PageMovement::End);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Resize => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
|
@ -430,6 +579,11 @@ impl Component for JobManager {
|
|||
Shortcuts::GENERAL,
|
||||
context.settings.shortcuts.general.key_values(),
|
||||
);
|
||||
map[Shortcuts::GENERAL].insert("sort by 1st column", Key::Char('1'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 2nd column", Key::Char('2'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 3rd column", Key::Char('3'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 4th column", Key::Char('4'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 5th column", Key::Char('5'));
|
||||
|
||||
map
|
||||
}
|
||||
|
@ -443,6 +597,10 @@ impl Component for JobManager {
|
|||
}
|
||||
|
||||
fn status(&self, _context: &Context) -> String {
|
||||
format!("{} entries", self.entries.len())
|
||||
format!(
|
||||
"{} entries. Use `sort <n> [asc/desc]` command or press column index number key \
|
||||
(twice to toggle asc/desc) to sort",
|
||||
self.entries.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,15 @@ pub fn pos_dec(p: Pos, dec: (usize, usize)) -> Pos {
|
|||
/// ```
|
||||
pub type Area = (Pos, Pos);
|
||||
|
||||
#[inline(always)]
|
||||
pub fn skip_rows(area: Area, n: usize) -> Option<Area> {
|
||||
let (upper_left, bottom_right) = area;
|
||||
if upper_left.1 + n <= bottom_right.1 {
|
||||
return Some((pos_inc(upper_left, (0, n)), bottom_right));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get an area's height
|
||||
///
|
||||
/// Example:
|
||||
|
|
|
@ -159,6 +159,15 @@ impl<const N: usize> Default for DataColumns<N> {
|
|||
}
|
||||
|
||||
impl<const N: usize> DataColumns<N> {
|
||||
pub const ARROW_UP: &str = "🠉";
|
||||
pub const ARROW_DOWN: &str = "🠋";
|
||||
pub const ARROW_UP_ASCII: &str = "^";
|
||||
pub const ARROW_DOWN_ASCII: &str = "v";
|
||||
// const ARROW_UP_1: &str = "↑";
|
||||
// const ARROW_DOWN_1: &str = "↓";
|
||||
// const ARROW_UP_3: &str = "▲";
|
||||
// const ARROW_DOWN_4: &str = "▼";
|
||||
|
||||
pub fn recalc_widths(
|
||||
&mut self,
|
||||
(screen_width, screen_height): (usize, usize),
|
||||
|
|
Loading…
Reference in New Issue