mirror of
https://github.com/kdheepak/taskwarrior-tui.git
synced 2025-08-27 15:47:19 +02:00
Add multiple selection
This commit is contained in:
parent
e5a8094105
commit
6311cc3f97
5 changed files with 241 additions and 84 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -808,6 +808,7 @@ dependencies = [
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"shlex",
|
"shlex",
|
||||||
"task-hookrs",
|
"task-hookrs",
|
||||||
|
"tinyset",
|
||||||
"tui",
|
"tui",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
|
@ -842,6 +843,16 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyset"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "784f540960a63144d63992caf430ed87e39d920f2c474cb8ac586ff31fb861fc"
|
||||||
|
dependencies = [
|
||||||
|
"itertools",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui"
|
name = "tui"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|
|
@ -36,6 +36,7 @@ rustyline = "7.1.0"
|
||||||
uuid = { version = "0.8.1", features = ["serde", "v4"] }
|
uuid = { version = "0.8.1", features = ["serde", "v4"] }
|
||||||
better-panic = "0.2.0"
|
better-panic = "0.2.0"
|
||||||
shellexpand = "2.1"
|
shellexpand = "2.1"
|
||||||
|
tinyset = "0.4"
|
||||||
|
|
||||||
[package.metadata.rpm]
|
[package.metadata.rpm]
|
||||||
package = "taskwarrior-tui"
|
package = "taskwarrior-tui"
|
||||||
|
|
181
src/app.rs
181
src/app.rs
|
@ -4,7 +4,7 @@ use crate::config::Config;
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::help::Help;
|
use crate::help::Help;
|
||||||
use crate::keyconfig::KeyConfig;
|
use crate::keyconfig::KeyConfig;
|
||||||
use crate::table::{Row, Table, TableState};
|
use crate::table::{Row, Table, TableMode, TableState};
|
||||||
use crate::task_report::TaskReportTable;
|
use crate::task_report::TaskReportTable;
|
||||||
use crate::util::Key;
|
use crate::util::Key;
|
||||||
use crate::util::{Event, Events};
|
use crate::util::{Event, Events};
|
||||||
|
@ -320,10 +320,22 @@ impl TTApp {
|
||||||
self.draw_task_details(f, split_task_layout[1]);
|
self.draw_task_details(f, split_task_layout[1]);
|
||||||
}
|
}
|
||||||
let selected = self.task_table_state.selected().unwrap_or_default();
|
let selected = self.task_table_state.selected().unwrap_or_default();
|
||||||
let task_id = if tasks_len == 0 {
|
let task_ids = if tasks_len == 0 {
|
||||||
0
|
vec!["0".to_string()]
|
||||||
} else {
|
} else {
|
||||||
self.tasks.lock().unwrap()[selected].id().unwrap_or_default()
|
match self.task_table_state.mode() {
|
||||||
|
TableMode::SingleSelection => vec![self.tasks.lock().unwrap()[selected]
|
||||||
|
.id()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()],
|
||||||
|
TableMode::MultipleSelection => {
|
||||||
|
let mut tids = vec![];
|
||||||
|
for s in self.task_table_state.marked() {
|
||||||
|
tids.push(self.tasks.lock().unwrap()[s].id().unwrap_or_default().to_string());
|
||||||
|
}
|
||||||
|
tids
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match self.mode {
|
match self.mode {
|
||||||
AppMode::TaskReport => self.draw_command(f, rects[1], self.filter.as_str(), "Filter Tasks"),
|
AppMode::TaskReport => self.draw_command(f, rects[1], self.filter.as_str(), "Filter Tasks"),
|
||||||
|
@ -346,10 +358,7 @@ impl TTApp {
|
||||||
f,
|
f,
|
||||||
rects[1],
|
rects[1],
|
||||||
self.command.as_str(),
|
self.command.as_str(),
|
||||||
Span::styled(
|
Span::styled("Log Tasks", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
format!("Modify Task {}", task_id).as_str(),
|
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
AppMode::TaskSubprocess => {
|
AppMode::TaskSubprocess => {
|
||||||
|
@ -360,32 +369,39 @@ impl TTApp {
|
||||||
f,
|
f,
|
||||||
rects[1],
|
rects[1],
|
||||||
self.command.as_str(),
|
self.command.as_str(),
|
||||||
Span::styled("Log Tasks", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
AppMode::TaskModify => {
|
AppMode::TaskModify => {
|
||||||
let position = self.get_position(&self.modify);
|
let position = self.get_position(&self.modify);
|
||||||
f.set_cursor(rects[1].x + position as u16 + 1, rects[1].y + 1);
|
f.set_cursor(rects[1].x + position as u16 + 1, rects[1].y + 1);
|
||||||
f.render_widget(Clear, rects[1]);
|
f.render_widget(Clear, rects[1]);
|
||||||
|
let label = if task_ids.len() > 1 {
|
||||||
|
format!("Modify Tasks {}", task_ids.join(","))
|
||||||
|
} else {
|
||||||
|
format!("Modify Task {}", task_ids.join(","))
|
||||||
|
};
|
||||||
self.draw_command(
|
self.draw_command(
|
||||||
f,
|
f,
|
||||||
rects[1],
|
rects[1],
|
||||||
self.modify.as_str(),
|
self.modify.as_str(),
|
||||||
Span::styled("Shell Command", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled(label, Style::default().add_modifier(Modifier::BOLD)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
AppMode::TaskAnnotate => {
|
AppMode::TaskAnnotate => {
|
||||||
let position = self.get_position(&self.command);
|
let position = self.get_position(&self.command);
|
||||||
f.set_cursor(rects[1].x + position as u16 + 1, rects[1].y + 1);
|
f.set_cursor(rects[1].x + position as u16 + 1, rects[1].y + 1);
|
||||||
f.render_widget(Clear, rects[1]);
|
f.render_widget(Clear, rects[1]);
|
||||||
|
let label = if task_ids.len() > 1 {
|
||||||
|
format!("Annotate Tasks {}", task_ids.join(","))
|
||||||
|
} else {
|
||||||
|
format!("Annotate Task {}", task_ids.join(","))
|
||||||
|
};
|
||||||
self.draw_command(
|
self.draw_command(
|
||||||
f,
|
f,
|
||||||
rects[1],
|
rects[1],
|
||||||
self.command.as_str(),
|
self.command.as_str(),
|
||||||
Span::styled(
|
Span::styled(label, Style::default().add_modifier(Modifier::BOLD)),
|
||||||
format!("Annotate Task {}", task_id).as_str(),
|
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
AppMode::TaskAdd => {
|
AppMode::TaskAdd => {
|
||||||
|
@ -656,6 +672,9 @@ impl TTApp {
|
||||||
if header == "ID" || header == "Name" {
|
if header == "ID" || header == "Name" {
|
||||||
// always give ID a couple of extra for indicator
|
// always give ID a couple of extra for indicator
|
||||||
widths[i] += self.config.uda_selection_indicator.as_str().graphemes(true).count();
|
widths[i] += self.config.uda_selection_indicator.as_str().graphemes(true).count();
|
||||||
|
if let TableMode::MultipleSelection = self.task_table_state.mode() {
|
||||||
|
widths[i] += 2
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1037,7 +1056,7 @@ impl TTApp {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let shell = self.command.as_str().replace("'", "\\'");
|
let shell = self.command.as_str();
|
||||||
|
|
||||||
match shlex::split(&shell) {
|
match shlex::split(&shell) {
|
||||||
Some(cmd) => {
|
Some(cmd) => {
|
||||||
|
@ -1072,7 +1091,7 @@ impl TTApp {
|
||||||
|
|
||||||
command.arg("log");
|
command.arg("log");
|
||||||
|
|
||||||
let shell = self.command.as_str().replace("'", "\\'");
|
let shell = self.command.as_str();
|
||||||
|
|
||||||
match shlex::split(&shell) {
|
match shlex::split(&shell) {
|
||||||
Some(cmd) => {
|
Some(cmd) => {
|
||||||
|
@ -1091,10 +1110,7 @@ impl TTApp {
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Err(format!(
|
None => Err(format!("Unable to run `task log`. Cannot shlex split `{}`", shell)),
|
||||||
"Unable to run `task log`. Cannot shlex split `{}`",
|
|
||||||
shell.as_str()
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1102,16 +1118,26 @@ impl TTApp {
|
||||||
if self.tasks.lock().unwrap().is_empty() {
|
if self.tasks.lock().unwrap().is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let selected = self.task_table_state.selected().unwrap_or_default();
|
let selected = match self.task_table_state.mode() {
|
||||||
let task_id = self.tasks.lock().unwrap()[selected].id().unwrap_or_default();
|
TableMode::SingleSelection => vec![self.task_table_state.selected().unwrap_or_default()],
|
||||||
let task_uuid = *self.tasks.lock().unwrap()[selected].uuid();
|
TableMode::MultipleSelection => self.task_table_state.marked().collect::<Vec<usize>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut task_uuids = vec![];
|
||||||
|
|
||||||
|
for s in selected {
|
||||||
|
let task_id = self.tasks.lock().unwrap()[s].id().unwrap_or_default();
|
||||||
|
let task_uuid = *self.tasks.lock().unwrap()[s].uuid();
|
||||||
|
task_uuids.push(task_uuid.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let shell = &self.config.uda_shortcuts[s];
|
let shell = &self.config.uda_shortcuts[s];
|
||||||
|
|
||||||
if shell.is_empty() {
|
if shell.is_empty() {
|
||||||
return Err("Trying to run empty shortcut.".to_string());
|
return Err("Trying to run empty shortcut.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let shell = format!("{} {}", shell, task_uuid);
|
let shell = format!("{} {}", shell, task_uuids.join(" "));
|
||||||
let shell = shellexpand::tilde(&shell).into_owned();
|
let shell = shellexpand::tilde(&shell).into_owned();
|
||||||
match shlex::split(&shell) {
|
match shlex::split(&shell) {
|
||||||
Some(cmd) => {
|
Some(cmd) => {
|
||||||
|
@ -1143,14 +1169,28 @@ impl TTApp {
|
||||||
if self.tasks.lock().unwrap().is_empty() {
|
if self.tasks.lock().unwrap().is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let selected = self.task_table_state.selected().unwrap_or_default();
|
|
||||||
let task_id = self.tasks.lock().unwrap()[selected].id().unwrap_or_default();
|
|
||||||
let task_uuid = *self.tasks.lock().unwrap()[selected].uuid();
|
|
||||||
let mut command = Command::new("task");
|
|
||||||
command.arg("rc.confirmation=off");
|
|
||||||
command.arg(format!("{}", task_uuid)).arg("modify");
|
|
||||||
|
|
||||||
let shell = self.modify.as_str().replace("'", "\\'");
|
let selected = match self.task_table_state.mode() {
|
||||||
|
TableMode::SingleSelection => vec![self.task_table_state.selected().unwrap_or_default()],
|
||||||
|
TableMode::MultipleSelection => self.task_table_state.marked().collect::<Vec<usize>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut task_uuids = vec![];
|
||||||
|
|
||||||
|
for s in selected {
|
||||||
|
let task_id = self.tasks.lock().unwrap()[s].id().unwrap_or_default();
|
||||||
|
let task_uuid = *self.tasks.lock().unwrap()[s].uuid();
|
||||||
|
task_uuids.push(task_uuid.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut command = Command::new("task");
|
||||||
|
command.arg("rc.bulk=0");
|
||||||
|
command.arg("rc.confirmation=off");
|
||||||
|
command.arg("rc.dependency.confirmation=off");
|
||||||
|
command.arg("rc.recurrence.confirmation=off");
|
||||||
|
command.arg(task_uuids.join(" ")).arg("modify");
|
||||||
|
|
||||||
|
let shell = self.modify.as_str();
|
||||||
|
|
||||||
match shlex::split(&shell) {
|
match shlex::split(&shell) {
|
||||||
Some(cmd) => {
|
Some(cmd) => {
|
||||||
|
@ -1164,23 +1204,16 @@ impl TTApp {
|
||||||
self.modify.update("", 0);
|
self.modify.update("", 0);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!("Modify failed. {}", String::from_utf8_lossy(&o.stdout),))
|
||||||
"Unable to modify task with uuid {}. Failed with status code {}",
|
|
||||||
task_uuid,
|
|
||||||
o.status.code().unwrap()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => Err(format!(
|
Err(_) => Err(format!(
|
||||||
"Cannot run `task {} modify {}`. Check documentation for more information",
|
"Cannot run `task {:?} modify {}`. Check documentation for more information",
|
||||||
task_uuid, shell,
|
task_uuids, shell,
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Err(format!(
|
None => Err(format!("Cannot shlex split `{}`", shell,)),
|
||||||
"Unable to run `task {} modify`. Cannot shlex split `{}`",
|
|
||||||
task_uuid, shell,
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1188,13 +1221,28 @@ impl TTApp {
|
||||||
if self.tasks.lock().unwrap().is_empty() {
|
if self.tasks.lock().unwrap().is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let selected = self.task_table_state.selected().unwrap_or_default();
|
|
||||||
let task_id = self.tasks.lock().unwrap()[selected].id().unwrap_or_default();
|
|
||||||
let task_uuid = *self.tasks.lock().unwrap()[selected].uuid();
|
|
||||||
let mut command = Command::new("task");
|
|
||||||
command.arg(format!("{}", task_uuid)).arg("annotate");
|
|
||||||
|
|
||||||
let shell = self.command.as_str().replace("'", "\\'");
|
let selected = match self.task_table_state.mode() {
|
||||||
|
TableMode::SingleSelection => vec![self.task_table_state.selected().unwrap_or_default()],
|
||||||
|
TableMode::MultipleSelection => self.task_table_state.marked().collect::<Vec<usize>>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut task_uuids = vec![];
|
||||||
|
|
||||||
|
for s in selected {
|
||||||
|
let task_id = self.tasks.lock().unwrap()[s].id().unwrap_or_default();
|
||||||
|
let task_uuid = *self.tasks.lock().unwrap()[s].uuid();
|
||||||
|
task_uuids.push(task_uuid.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut command = Command::new("task");
|
||||||
|
command.arg("rc.bulk=0");
|
||||||
|
command.arg("rc.confirmation=off");
|
||||||
|
command.arg("rc.dependency.confirmation=off");
|
||||||
|
command.arg("rc.recurrence.confirmation=off");
|
||||||
|
command.arg(task_uuids.join(" ")).arg("annotate");
|
||||||
|
|
||||||
|
let shell = self.command.as_str();
|
||||||
|
|
||||||
match shlex::split(&shell) {
|
match shlex::split(&shell) {
|
||||||
Some(cmd) => {
|
Some(cmd) => {
|
||||||
|
@ -1208,23 +1256,17 @@ impl TTApp {
|
||||||
self.command.update("", 0);
|
self.command.update("", 0);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(format!("Annotate failed. {}", String::from_utf8_lossy(&o.stdout),))
|
||||||
"Unable to annotate task with uuid {}. Failed with status code {}",
|
|
||||||
task_uuid,
|
|
||||||
o.status.code().unwrap()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => Err(format!(
|
Err(_) => Err(format!(
|
||||||
"Cannot run `task {} annotate {}`. Check documentation for more information",
|
"Cannot run `task {} annotate {}`. Check documentation for more information",
|
||||||
task_uuid, shell
|
task_uuids.join(" "),
|
||||||
|
shell
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Err(format!(
|
None => Err(format!("Cannot shlex split `{}`", shell)),
|
||||||
"Unable to run `task {} annotate`. Cannot shlex split `{}`",
|
|
||||||
task_uuid, shell
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1538,8 +1580,14 @@ impl TTApp {
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
AppMode::TaskReport => {
|
AppMode::TaskReport => {
|
||||||
if input == self.keyconfig.quit || input == Key::Ctrl('c') {
|
if input == Key::Esc {
|
||||||
|
self.task_table_state.single_selection();
|
||||||
|
self.task_table_state.clear();
|
||||||
|
} else if input == self.keyconfig.quit || input == Key::Ctrl('c') {
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
|
} else if input == self.keyconfig.select {
|
||||||
|
self.task_table_state.multiple_selection();
|
||||||
|
self.task_table_state.toggle_mark(self.task_table_state.selected());
|
||||||
} else if input == self.keyconfig.refresh {
|
} else if input == self.keyconfig.refresh {
|
||||||
self.update(true)?;
|
self.update(true)?;
|
||||||
} else if input == self.keyconfig.go_to_bottom || input == Key::End {
|
} else if input == self.keyconfig.go_to_bottom || input == Key::End {
|
||||||
|
@ -1603,12 +1651,15 @@ impl TTApp {
|
||||||
}
|
}
|
||||||
} else if input == self.keyconfig.modify {
|
} else if input == self.keyconfig.modify {
|
||||||
self.mode = AppMode::TaskModify;
|
self.mode = AppMode::TaskModify;
|
||||||
match self.task_current() {
|
match self.task_table_state.mode() {
|
||||||
Some(t) => {
|
TableMode::SingleSelection => match self.task_current() {
|
||||||
let s = format!("{} ", t.description());
|
Some(t) => {
|
||||||
self.modify.update(&s, s.as_str().len())
|
let s = format!("{} ", t.description());
|
||||||
}
|
self.modify.update(&s, s.as_str().len())
|
||||||
None => self.modify.update("", 0),
|
}
|
||||||
|
None => self.modify.update("", 0),
|
||||||
|
},
|
||||||
|
TableMode::MultipleSelection => self.modify.update("", 0),
|
||||||
}
|
}
|
||||||
} else if input == self.keyconfig.shell {
|
} else if input == self.keyconfig.shell {
|
||||||
self.mode = AppMode::TaskSubprocess;
|
self.mode = AppMode::TaskSubprocess;
|
||||||
|
|
|
@ -18,6 +18,7 @@ pub struct KeyConfig {
|
||||||
pub delete: Key,
|
pub delete: Key,
|
||||||
pub done: Key,
|
pub done: Key,
|
||||||
pub start_stop: Key,
|
pub start_stop: Key,
|
||||||
|
pub select: Key,
|
||||||
pub undo: Key,
|
pub undo: Key,
|
||||||
pub edit: Key,
|
pub edit: Key,
|
||||||
pub modify: Key,
|
pub modify: Key,
|
||||||
|
@ -57,6 +58,7 @@ impl Default for KeyConfig {
|
||||||
delete: Key::Char('x'),
|
delete: Key::Char('x'),
|
||||||
done: Key::Char('d'),
|
done: Key::Char('d'),
|
||||||
start_stop: Key::Char('s'),
|
start_stop: Key::Char('s'),
|
||||||
|
select: Key::Char('v'),
|
||||||
undo: Key::Char('u'),
|
undo: Key::Char('u'),
|
||||||
edit: Key::Char('e'),
|
edit: Key::Char('e'),
|
||||||
modify: Key::Char('m'),
|
modify: Key::Char('m'),
|
||||||
|
@ -117,6 +119,9 @@ impl KeyConfig {
|
||||||
self.start_stop = self
|
self.start_stop = self
|
||||||
.get_config("uda.taskwarrior-tui.keyconfig.start-stop")
|
.get_config("uda.taskwarrior-tui.keyconfig.start-stop")
|
||||||
.unwrap_or(self.start_stop);
|
.unwrap_or(self.start_stop);
|
||||||
|
self.select = self
|
||||||
|
.get_config("uda.taskwarrior-tui.keyconfig.select")
|
||||||
|
.unwrap_or(self.select);
|
||||||
self.undo = self
|
self.undo = self
|
||||||
.get_config("uda.taskwarrior-tui.keyconfig.undo")
|
.get_config("uda.taskwarrior-tui.keyconfig.undo")
|
||||||
.unwrap_or(self.undo);
|
.unwrap_or(self.undo);
|
||||||
|
@ -164,6 +169,7 @@ impl KeyConfig {
|
||||||
&self.page_up,
|
&self.page_up,
|
||||||
&self.delete,
|
&self.delete,
|
||||||
&self.done,
|
&self.done,
|
||||||
|
&self.select,
|
||||||
&self.start_stop,
|
&self.start_stop,
|
||||||
&self.undo,
|
&self.undo,
|
||||||
&self.edit,
|
&self.edit,
|
||||||
|
|
126
src/table.rs
126
src/table.rs
|
@ -8,6 +8,7 @@ use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
iter::{self, Iterator},
|
iter::{self, Iterator},
|
||||||
};
|
};
|
||||||
|
use tinyset::SetUsize;
|
||||||
use tui::{
|
use tui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Constraint, Rect},
|
layout::{Constraint, Rect},
|
||||||
|
@ -19,31 +20,69 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TableMode {
|
||||||
|
SingleSelection,
|
||||||
|
MultipleSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct TableState {
|
pub struct TableState {
|
||||||
offset: usize,
|
offset: usize,
|
||||||
selected: Option<usize>,
|
current_selection: Option<usize>,
|
||||||
|
selected: SetUsize,
|
||||||
|
mode: TableMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TableState {
|
impl Default for TableState {
|
||||||
fn default() -> TableState {
|
fn default() -> TableState {
|
||||||
TableState {
|
TableState {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
selected: None,
|
current_selection: None,
|
||||||
|
selected: SetUsize::new(),
|
||||||
|
mode: TableMode::SingleSelection,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableState {
|
impl TableState {
|
||||||
|
pub fn mode(&self) -> TableMode {
|
||||||
|
self.mode.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multiple_selection(&mut self) {
|
||||||
|
self.mode = TableMode::MultipleSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn single_selection(&mut self) {
|
||||||
|
self.mode = TableMode::SingleSelection;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected(&self) -> Option<usize> {
|
pub fn selected(&self) -> Option<usize> {
|
||||||
self.selected
|
self.current_selection
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select(&mut self, index: Option<usize>) {
|
pub fn select(&mut self, index: Option<usize>) {
|
||||||
self.selected = index;
|
self.current_selection = index;
|
||||||
if index.is_none() {
|
if index.is_none() {
|
||||||
self.offset = 0;
|
self.offset = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_mark(&mut self, index: Option<usize>) {
|
||||||
|
if let Some(i) = index {
|
||||||
|
if !self.selected.insert(i) {
|
||||||
|
self.selected.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn marked(&self) -> impl Iterator<Item = usize> + '_ {
|
||||||
|
self.selected.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.selected.drain().for_each(drop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Holds data to be displayed in a Table widget
|
/// Holds data to be displayed in a Table widget
|
||||||
|
@ -99,8 +138,10 @@ pub struct Table<'a, H, R> {
|
||||||
header_gap: u16,
|
header_gap: u16,
|
||||||
/// Style used to render the selected row
|
/// Style used to render the selected row
|
||||||
highlight_style: Style,
|
highlight_style: Style,
|
||||||
/// Symbol in front of the selected rom
|
/// Symbol in front of the selected row
|
||||||
highlight_symbol: Option<&'a str>,
|
highlight_symbol: Option<&'a str>,
|
||||||
|
/// Symbol in front of the marked row
|
||||||
|
mark_symbol: Option<&'a str>,
|
||||||
/// Data to display in each row
|
/// Data to display in each row
|
||||||
rows: R,
|
rows: R,
|
||||||
}
|
}
|
||||||
|
@ -121,6 +162,7 @@ where
|
||||||
header_gap: 1,
|
header_gap: 1,
|
||||||
highlight_style: Style::default(),
|
highlight_style: Style::default(),
|
||||||
highlight_symbol: None,
|
highlight_symbol: None,
|
||||||
|
mark_symbol: None,
|
||||||
rows: R::default(),
|
rows: R::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,6 +185,7 @@ where
|
||||||
header_gap: 1,
|
header_gap: 1,
|
||||||
highlight_style: Style::default(),
|
highlight_style: Style::default(),
|
||||||
highlight_symbol: None,
|
highlight_symbol: None,
|
||||||
|
mark_symbol: None,
|
||||||
rows,
|
rows,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,12 +347,32 @@ where
|
||||||
y += 1 + self.header_gap;
|
y += 1 + self.header_gap;
|
||||||
|
|
||||||
// Use highlight_style only if something is selected
|
// Use highlight_style only if something is selected
|
||||||
let (selected, highlight_style) = match state.selected {
|
let (selected, highlight_style) = if state.selected().is_some() {
|
||||||
Some(i) => (Some(i), self.highlight_style),
|
(state.selected(), self.highlight_style)
|
||||||
None => (None, self.style),
|
} else {
|
||||||
|
(None, self.style)
|
||||||
|
};
|
||||||
|
|
||||||
|
let highlight_symbol = match state.mode {
|
||||||
|
TableMode::MultipleSelection => {
|
||||||
|
let s = self.highlight_symbol.unwrap_or("• ").trim_end();
|
||||||
|
format!("[{}] ", s)
|
||||||
|
}
|
||||||
|
TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mark_symbol = match state.mode {
|
||||||
|
TableMode::MultipleSelection => {
|
||||||
|
let s = self.mark_symbol.unwrap_or("x ").trim_end();
|
||||||
|
format!("[{}] ", s)
|
||||||
|
}
|
||||||
|
TableMode::SingleSelection => self.highlight_symbol.unwrap_or("").to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let blank_symbol = match state.mode {
|
||||||
|
TableMode::MultipleSelection => "[ ] ".to_string(),
|
||||||
|
TableMode::SingleSelection => iter::repeat(" ").take(highlight_symbol.width()).collect::<String>(),
|
||||||
};
|
};
|
||||||
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
|
||||||
let blank_symbol = iter::repeat(" ").take(highlight_symbol.width()).collect::<String>();
|
|
||||||
|
|
||||||
// Draw rows
|
// Draw rows
|
||||||
let default_style = Style::default();
|
let default_style = Style::default();
|
||||||
|
@ -317,11 +380,11 @@ where
|
||||||
let remaining = (table_area.bottom() - y) as usize;
|
let remaining = (table_area.bottom() - y) as usize;
|
||||||
|
|
||||||
// Make sure the table shows the selected item
|
// Make sure the table shows the selected item
|
||||||
state.offset = if let Some(selected) = selected {
|
state.offset = if let Some(s) = selected {
|
||||||
if selected >= remaining + state.offset - 1 {
|
if s >= remaining + state.offset - 1 {
|
||||||
selected + 1 - remaining
|
s + 1 - remaining
|
||||||
} else if selected < state.offset {
|
} else if s < state.offset {
|
||||||
selected
|
s
|
||||||
} else {
|
} else {
|
||||||
state.offset
|
state.offset
|
||||||
}
|
}
|
||||||
|
@ -330,11 +393,32 @@ where
|
||||||
};
|
};
|
||||||
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
|
for (i, row) in self.rows.skip(state.offset).take(remaining).enumerate() {
|
||||||
let (data, style, symbol) = match row {
|
let (data, style, symbol) = match row {
|
||||||
Row::Data(d) | Row::StyledData(d, _) if Some(i) == state.selected.map(|s| s - state.offset) => {
|
Row::Data(d) | Row::StyledData(d, _) if Some(i) == state.selected().map(|s| s - state.offset) => {
|
||||||
(d, highlight_style, highlight_symbol)
|
match state.mode {
|
||||||
|
TableMode::MultipleSelection => {
|
||||||
|
if state.selected.contains(i) {
|
||||||
|
(d, highlight_style, mark_symbol.to_string())
|
||||||
|
} else {
|
||||||
|
(d, highlight_style, blank_symbol.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TableMode::SingleSelection => (d, highlight_style, highlight_symbol.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row::Data(d) => {
|
||||||
|
if state.selected.contains(i) {
|
||||||
|
(d, default_style, mark_symbol.to_string())
|
||||||
|
} else {
|
||||||
|
(d, default_style, blank_symbol.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row::StyledData(d, s) => {
|
||||||
|
if state.selected.contains(i) {
|
||||||
|
(d, s, mark_symbol.to_string())
|
||||||
|
} else {
|
||||||
|
(d, s, blank_symbol.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Row::Data(d) => (d, default_style, blank_symbol.as_ref()),
|
|
||||||
Row::StyledData(d, s) => (d, s, blank_symbol.as_ref()),
|
|
||||||
};
|
};
|
||||||
x = table_area.left();
|
x = table_area.left();
|
||||||
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
|
for (c, (w, elt)) in solved_widths.iter().zip(data).enumerate() {
|
||||||
|
@ -347,6 +431,10 @@ where
|
||||||
style,
|
style,
|
||||||
);
|
);
|
||||||
if c == header_index {
|
if c == header_index {
|
||||||
|
let symbol = match state.mode {
|
||||||
|
TableMode::SingleSelection => &symbol,
|
||||||
|
TableMode::MultipleSelection => &symbol,
|
||||||
|
};
|
||||||
format!(
|
format!(
|
||||||
"{symbol}{elt:>width$}",
|
"{symbol}{elt:>width$}",
|
||||||
symbol = symbol,
|
symbol = symbol,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue