use {
crate::*,
anyhow::Result,
crokey::KeyCombination,
std::{
io::Write,
process::ExitStatus,
time::Instant,
},
termimad::{
Area,
CompoundStyle,
MadSkin,
crossterm::{
cursor,
execute,
style::{
Attribute,
Print,
},
},
minimad::{
Alignment,
Composite,
},
},
};
pub struct MissionState<'a, 'm> {
pub app_state: &'a mut AppState,
pub mission: Mission<'m>,
report_maker: ReportMaker,
output: Option<CommandOutput>,
wrapped_output: Option<WrappedCommandOutput>,
pub cmd_result: CommandResult,
wrapped_report: Option<WrappedReport>,
width: u16,
height: u16,
computing: bool,
pub wrap: bool,
pub backtrace: Option<&'static str>,
summary: bool,
reverse: bool,
status_skin: MadSkin,
scroll: usize,
top_item_idx: usize,
scrolled_to_top_item_idx: Option<usize>,
help_line: Option<HelpLine>,
help_page: Option<HelpPage>,
raw_output: bool,
pub auto_refresh: AutoRefresh,
pub changes_since_last_job_start: usize,
pub show_changes_count: bool,
pub messages: Vec<Message>,
pub search: SearchState,
pub dialog: Dialog,
pub scroll_anchor: ScrollAnchor,
}
impl<'a, 'm> MissionState<'a, 'm> {
pub fn new(
app_state: &'a mut AppState,
mission: Mission<'m>,
) -> Result<Self> {
let report_maker = ReportMaker::new(&mission);
let mut status_skin = MadSkin::default();
let skin = mission.job.skin;
status_skin
.paragraph
.set_fgbg(skin.status_fg.color(), skin.status_bg.color());
status_skin.italic = CompoundStyle::new(
Some(skin.status_key_fg.color()),
None,
Attribute::Bold.into(),
);
let (width, height) = if app_state.headless {
(50, 50)
} else {
termimad::terminal_size()
};
let help_line = mission
.settings
.help_line
.then(|| HelpLine::new(mission.settings));
let show_changes_count = mission.job.show_changes_count();
let scroll_anchor = mission.job.scroll_anchor();
Ok(Self {
report_maker,
output: None,
wrapped_output: None,
cmd_result: CommandResult::None,
wrapped_report: None,
width,
height,
computing: true,
summary: mission.settings.summary,
wrap: mission.settings.wrap,
backtrace: None,
reverse: mission.settings.reverse,
show_changes_count,
status_skin,
scroll: 0,
scrolled_to_top_item_idx: None,
top_item_idx: 0,
help_line,
help_page: None,
raw_output: false,
auto_refresh: AutoRefresh::Enabled,
changes_since_last_job_start: 0,
messages: Vec::new(),
search: Default::default(),
dialog: Dialog::None,
app_state,
mission,
scroll_anchor,
})
}
pub fn open_jobs_menu(&mut self) {
self.dialog = Dialog::Menu(ActionMenu::with_all_jobs(&self.mission));
}
pub fn open_menu(
&mut self,
def: ActionMenuDefinition,
) {
self.dialog = Dialog::Menu(ActionMenu::from_definition(def, self.mission.settings));
}
pub fn close_menu(&mut self) {
if let Dialog::Menu(_) = self.dialog {
self.dialog = Dialog::None;
}
}
pub fn focus_file(
&mut self,
ffc: &FocusFileCommand,
) {
if let CommandResult::Report(report) = &mut self.cmd_result {
report.focus_file(ffc);
if self.wrap {
self.wrapped_report = None;
self.update_wrap();
}
self.reset_scroll();
}
}
pub fn show_item(
&mut self,
item_idx: usize,
) {
let target_line_idx = self
.lines_to_draw()
.enumerate()
.find(|(_, line)| line.item_idx == item_idx)
.map(|(idx, _)| idx);
if let Some(line_idx) = target_line_idx {
self.scroll = line_idx;
self.fix_scroll();
self.top_item_idx = item_idx;
}
}
pub fn top_item_idx(&self) -> Option<usize> {
self.lines_to_draw()
.nth(self.scroll)
.map(|line| line.item_idx)
}
pub fn focus_search(&mut self) {
self.search.focus_with_mode(SearchMode::Pattern);
self.show_selected_found();
}
pub fn focus_goto(&mut self) {
self.search.focus_with_mode(SearchMode::ItemIdx);
self.show_selected_found();
}
pub fn dismiss_top_item(&mut self) -> bool {
if let Some(report) = self.cmd_result.report() {
if let Some(item_idx) = self.top_item_idx() {
if let Some(location) = report.item_location(item_idx) {
let location = location.to_string();
self.app_state.filter.add(Dismissal::Location(location));
self.apply_filter();
return true;
}
}
}
false
}
pub fn dismiss_top_item_type(&mut self) -> bool {
if let Some(report) = self.cmd_result.report() {
if let Some(item_idx) = self.top_item_idx() {
if let Some(diag_type) = report.item_diag_type(item_idx) {
let diag_type = diag_type.to_string();
self.app_state.filter.add(Dismissal::DiagType(diag_type));
self.apply_filter();
return true;
}
}
}
false
}
pub fn dismiss_top(&mut self) -> bool {
self.dismiss_top_item_type() || self.dismiss_top_item()
}
pub fn undismiss_all(&mut self) {
if let Some(report) = self.cmd_result.report_mut() {
self.app_state.filter.restore_dismissed_lines(report);
self.app_state.filter = Default::default();
self.apply_filter();
}
}
pub fn remove_dismissal(
&mut self,
dismissal: &Dismissal,
) {
self.app_state.filter.remove(dismissal);
if let Some(report) = self.cmd_result.report_mut() {
self.app_state.filter.restore_dismissed_lines(report);
self.apply_filter();
}
}
pub fn open_undismiss_menu(&mut self) {
self.dialog = Dialog::Menu(self.app_state.filter.undismiss_menu());
}
fn apply_filter(&mut self) {
let Some(report) = self.cmd_result.report_mut() else {
return;
};
if self.app_state.filter.remove_dismissed_lines(report) {
self.mission.settings.exports.do_auto_exports(self);
self.wrapped_report = None;
self.update_wrap();
}
}
pub fn back(&mut self) -> bool {
if self.dialog.is_some() {
self.dialog = Dialog::None; } else if self.search.focused() {
self.search.unfocus_and_clear();
} else if self.help_page.is_some() {
self.help_page = None;
} else if self.search.input_has_content() {
self.search.clear();
} else {
return false;
}
true
}
pub fn copy_unstyled_output(&mut self) {
let message = {
#[cfg(feature = "clipboard")]
match arboard::Clipboard::new() {
Ok(mut clipboard) => {
let mut content = String::new();
for line in self.lines_to_draw() {
content.push_str(&line.content.to_raw());
content.push('\n');
}
let _ = clipboard.set_text(content);
"Output copied to clipboard"
}
Err(e) => {
error!("Failed to copy output to clipboard: {}", e);
"Clipboard error - nothing copied"
}
}
#[cfg(not(feature = "clipboard"))]
"clipboard feature not enabled : nothing copied"
};
self.messages.push(Message::short(message));
}
pub fn next_match(&mut self) {
self.search.next_match();
self.show_selected_found();
}
pub fn previous_match(&mut self) {
self.search.previous_match();
self.show_selected_found();
}
pub fn validate(&mut self) -> bool {
if self.search.focused() {
self.search.unfocus();
true
} else {
false
}
}
pub fn has_search(&self) -> bool {
self.search.focused() || self.search.input_has_content()
}
pub fn on_key(
&mut self,
key: KeyCombination,
) -> Option<Action> {
match &mut self.dialog {
Dialog::None => {}
Dialog::Menu(menu) => match menu.state.on_key(key) {
(Some(action), true) => {
self.close_menu();
return Some(action);
}
(None, true) => {
return Some(Action::NoOp);
}
_ => {
return None;
}
},
}
if self.search.apply_key_combination(key) {
self.update_search();
self.show_selected_found();
return Some(Action::NoOp);
}
None
}
pub fn update_search(&mut self) {
if self.search.is_up_to_date() {
return;
}
let founds = if self.search.input_has_content() {
let search = self.search.search();
let lines = self.lines_to_draw();
search.search_lines(lines)
} else {
Vec::new()
};
self.search.set_founds(founds);
}
fn update_search_from_line(
&mut self,
line_count_before: usize,
) {
if !self.search.input_has_content() {
return;
}
let lines = self.lines_to_draw_unfiltered();
let search = self.search.search();
if line_count_before >= lines.len() {
warn!("inconsistent line_count_before");
return;
}
let new_founds = search.search_lines(&lines[line_count_before..]);
self.search.extend_founds(new_founds);
}
pub fn add_line(
&mut self,
line: CommandOutputLine,
) {
let auto_scroll = self.is_scroll_at_prefered_end();
let line_count_before = self.lines_to_draw_unfiltered().len();
if let Some(output) = self.output.as_mut() {
self.report_maker.receive_line(line, output);
if self.wrap {
self.update_wrap();
}
if auto_scroll {
self.scroll_to_prefered_end();
}
} else {
self.wrapped_output = None;
self.output = {
let mut output = CommandOutput::default();
self.report_maker.receive_line(line, &mut output);
Some(output)
};
if self.wrap {
self.update_wrap();
}
self.scroll = 0;
self.fix_scroll();
}
self.update_search_from_line(line_count_before);
}
pub fn new_task(&self) -> Task {
Task {
backtrace: self.backtrace,
grace_period: self.mission.job.grace_period(),
}
}
pub fn take_output(&mut self) -> Option<CommandOutput> {
self.search.touch();
self.wrapped_output = None;
self.output.take()
}
pub fn has_report(&self) -> bool {
matches!(self.cmd_result, CommandResult::Report(_))
}
pub fn has_dismissed_items(&self) -> bool {
self.cmd_result
.report()
.is_some_and(Report::has_dismissed_items)
}
pub fn can_be_scoped(&self) -> bool {
self.cmd_result
.report()
.is_some_and(Report::can_scope_tests)
}
pub fn failures_scope(&self) -> Option<Scope> {
if !self.can_be_scoped() {
return None;
}
self.cmd_result.report().map(|report| Scope {
tests: report.failure_keys.clone(),
})
}
pub fn toggle_raw_output(&mut self) {
self.raw_output ^= true;
if self.wrapped_output.is_some() {
self.wrapped_output = None;
}
if self.wrap {
self.update_wrap();
}
self.search.touch();
}
pub fn finish_task(
&mut self,
exit_status: ExitStatus,
) -> Result<()> {
let output = self.take_output().unwrap_or_default();
let result = self.report_maker.build_result(output, exit_status)?;
self.set_result(result);
Ok(())
}
fn set_result(
&mut self,
mut cmd_result: CommandResult,
) {
self.search.touch();
if self.reverse {
cmd_result.reverse();
}
match &cmd_result {
CommandResult::Report(report) => {
info!("Got report - Stats: {:#?}", report.stats);
}
CommandResult::Failure(_) => {
debug!("Got failure");
}
CommandResult::None => {
debug!("GOT NONE ???");
}
}
if let CommandResult::Report(ref mut report) = cmd_result {
if report
.lines
.last()
.is_some_and(|line| line.content.is_blank())
{
report.lines.pop();
}
}
let fix_scroll = self.cmd_result.lines_len() != cmd_result.lines_len();
self.wrapped_report = None;
self.wrapped_output = None;
self.cmd_result = cmd_result;
self.apply_filter();
self.computing = false;
self.raw_output = false;
if self.wrap {
self.update_wrap();
}
if fix_scroll {
if self.scrolled_to_top_item_idx == Some(self.top_item_idx) {
self.show_item(self.top_item_idx);
} else {
self.scrolled_to_top_item_idx = None;
self.reset_scroll();
}
}
self.mission.settings.exports.do_auto_exports(self);
}
pub fn is_computing(&self) -> bool {
self.computing
}
pub fn clear(&mut self) {
debug!("state.clear");
self.take_output();
self.cmd_result = CommandResult::None;
self.search.touch();
}
pub fn start_computation(
&mut self,
executor: &mut MissionExecutor,
) -> Result<TaskExecutor> {
debug!("state.start_computation");
self.computation_starts();
executor.start(self.new_task())
}
pub fn computation_starts(&mut self) {
if !self.mission.job.background() {
self.clear();
}
self.report_maker.start(&self.mission);
self.computing = true;
self.changes_since_last_job_start = 0;
self.search.touch();
}
pub fn computation_stops(&mut self) {
self.computing = false;
}
pub fn receive_watch_event(&mut self) {
self.changes_since_last_job_start += 1;
}
fn scroll_to_top(&mut self) {
self.scroll = 0;
self.top_item_idx = 0;
}
fn scroll_to_bottom(&mut self) {
let ch = self.content_height();
let ph = self.page_height();
self.scroll = ch.saturating_sub(ph);
self.top_item_idx = self.top_item_idx().unwrap_or(0);
}
fn scroll_to_end(
&mut self,
end: ScrollEnd,
) {
if end.is_top(self.reverse) {
self.scroll_to_top();
} else {
self.scroll_to_bottom();
}
}
fn scroll_to_prefered_end(&mut self) {
let end = self.prefered_scroll_end();
self.scroll_to_end(end);
}
fn current_scroll_end(&self) -> Option<ScrollEnd> {
if self.is_scroll_at_top() {
Some(ScrollEnd::First)
} else if self.is_scroll_at_bottom() {
Some(ScrollEnd::Last)
} else {
None
}
}
fn is_scroll_at_top(&self) -> bool {
self.scroll == 0
}
fn is_scroll_at_bottom(&self) -> bool {
self.scroll + self.page_height() + 1 >= self.content_height()
}
fn is_scroll_at_prefered_end(&self) -> bool {
if self.prefered_scroll_end().is_top(self.reverse) {
self.is_scroll_at_top()
} else {
self.is_scroll_at_bottom()
}
}
fn reset_scroll(&mut self) {
self.scroll_to_prefered_end();
}
fn fix_scroll(&mut self) {
self.scroll = fix_scroll(self.scroll, self.content_height(), self.page_height());
}
pub fn keybindings(&self) -> &KeyBindings {
&self.mission.settings.keybindings
}
fn show_line(
&mut self,
line_idx: usize,
) {
let page_height = self.page_height();
if line_idx < self.scroll || line_idx >= self.scroll + page_height {
self.scroll = (line_idx - (page_height / 2).min(line_idx))
.min(self.content_height().max(page_height) - page_height);
}
}
fn show_selected_found(&mut self) {
if let Some(selected_line) = self.search.selected_found_line() {
self.show_line(selected_line);
}
}
pub fn close_help(&mut self) -> bool {
if self.help_page.is_some() {
self.help_page = None;
true
} else {
false
}
}
pub fn is_help(&self) -> bool {
self.help_page.is_some()
}
pub fn toggle_help(&mut self) {
self.help_page = match self.help_page {
Some(_) => None,
None => Some(HelpPage::new(self.mission.settings)),
};
}
pub fn toggle_summary_mode(&mut self) {
let visible_state = self.visible_scroll_state();
self.summary ^= true;
self.search.touch();
self.update_search();
self.restore_visible_scroll_state(visible_state);
self.show_selected_found();
}
pub fn toggle_backtrace(
&mut self,
level: &'static str,
) {
self.backtrace = if self.backtrace == Some(level) {
None
} else {
Some(level)
};
}
pub fn toggle_wrap_mode(&mut self) {
let visible_state = self.visible_scroll_state();
self.wrap ^= true;
if self.wrapped_output.is_some() {
self.wrapped_output = None;
}
if self.wrap {
self.update_wrap();
}
self.search.touch();
self.restore_visible_scroll_state(visible_state);
}
fn content_height(&self) -> usize {
let lines = self.lines_to_draw();
lines.count()
}
fn page_height(&self) -> usize {
self.height.max(3) as usize - 3
}
pub fn resize(
&mut self,
width: u16,
height: u16,
) {
let visible_state = self.visible_scroll_state();
if self.width != width {
self.wrapped_report = None;
self.wrapped_output = None;
}
self.width = width;
self.height = height;
if self.wrap {
self.update_wrap();
}
self.search.touch();
self.restore_visible_scroll_state(visible_state);
}
pub fn visible_scroll_state(&self) -> VisibleScrollState {
if let Some(scroll_end) = self.current_scroll_end() {
VisibleScrollState::ScrollEnd(scroll_end)
} else {
VisibleScrollState::TopItemIdx(self.top_item_idx)
}
}
pub fn restore_visible_scroll_state(
&mut self,
state: VisibleScrollState,
) {
match state {
VisibleScrollState::ScrollEnd(end) => self.scroll_to_end(end),
VisibleScrollState::TopItemIdx(idx) => self.show_item(idx),
}
}
pub fn apply_scroll_command(
&mut self,
cmd: ScrollCommand,
) {
if let Some(help_page) = self.help_page.as_mut() {
help_page.apply_scroll_command(cmd);
} else {
self.scroll = cmd.apply(self.scroll, self.content_height(), self.page_height());
self.scrolled_to_top_item_idx = self.top_item_idx();
}
}
fn draw_status_line(
&mut self,
w: &mut W,
y: u16,
) -> Result<()> {
let mut help_start = 0;
if self.search.must_be_drawn() {
let search_width = (self.width / 4).clamp(9, 27);
let skin = self.mission.job.skin;
let csi = format!("\u{1b}[1m\u{1b}[38;5;{}m", skin.search_input_prefix_fg());
self.search
.draw_prefixed_input(w, 0, y, &csi, search_width)?;
help_start += search_width;
}
goto(w, help_start, y)?;
if let Some(help_line) = &self.help_line {
let markdown = help_line.markdown(self);
if self.height > 1 {
let help_width = self.width - help_start;
self.status_skin.write_composite_fill(
w,
Composite::from_inline(&markdown),
help_width.into(),
Alignment::Left,
)?;
}
} else {
clear_line(w)?;
}
Ok(())
}
pub fn job_badges(&self) -> Vec<TString> {
let mut badges = Vec::new();
let project_name = &self.mission.location_name;
let skin = self.mission.job.skin;
badges.push(TString::badge(
project_name,
skin.project_name_badge_fg(),
skin.project_name_badge_bg(),
));
let job_label = self.mission.concrete_job_ref.badge_label();
badges.push(TString::badge(
&job_label,
skin.job_label_badge_fg(),
skin.job_label_badge_bg(),
));
if let CommandResult::Report(report) = &self.cmd_result {
let stats = &report.stats;
if stats.errors > 0 {
badges.push(TString::num_badge(
stats.errors,
"error",
skin.errors_badge_fg(),
skin.errors_badge_bg(),
));
}
if stats.test_fails > 0 {
badges.push(TString::num_badge(
stats.test_fails,
"fail",
skin.test_fails_badge_fg(),
skin.test_fails_badge_bg(),
));
} else if report.has_passed_tests {
badges.push(TString::badge(
"pass!",
skin.test_pass_badge_fg(),
skin.test_pass_badge_bg(),
));
}
if stats.warnings > 0 {
badges.push(TString::num_badge(
stats.warnings,
"warning",
skin.warnings_badge_fg(),
skin.warnings_badge_bg(),
));
}
if let Some(error_code) = report.error_code() {
if self.mission.job.show_command_error_code == Some(true) {
badges.push(TString::badge(
&format!("Command error code: {error_code}"),
skin.command_error_badge_fg(),
skin.command_error_badge_bg(),
));
}
}
} else if let CommandResult::Failure(failure) = &self.cmd_result {
badges.push(TString::badge(
&format!("Command error code: {}", failure.error_code),
skin.command_error_badge_fg(),
skin.command_error_badge_bg(),
));
}
badges
}
pub fn draw_badges(
&mut self,
w: &mut W,
y: u16,
) -> Result<usize> {
let skin = self.mission.job.skin;
goto_line(w, y)?;
let mut t_line = TLine::default();
for badge in self.job_badges() {
t_line.add_badge(badge);
}
let dismissed = self.report_to_draw().map_or(0, |r| r.dismissed_items);
if dismissed > 0 {
t_line.add_badge(TString::num_badge(
dismissed,
"dismissed item",
skin.dismissed_badge_fg(),
skin.dismissed_badge_bg(),
));
}
if self.show_changes_count {
t_line.add_badge(TString::num_badge(
self.changes_since_last_job_start,
"change",
skin.change_badge_fg(),
skin.change_badge_bg(),
));
}
if self.search.input_has_content() {
let csi_found = format!("\u{1b}[1m\u{1b}[38;5;{}m", skin.found_fg());
self.search.add_summary_tstring(&mut t_line, &csi_found);
}
let width = self.width as usize;
let cols = t_line.draw_in(w, width)?;
clear_line(w)?;
Ok(cols)
}
pub fn draw_computing(
&mut self,
w: &mut W,
y: u16,
) -> Result<()> {
goto_line(w, y)?;
let skin = self.mission.job.skin;
let width = self.width as usize;
if self.computing {
write!(
w,
"\u{1b}[38;5;{}m\u{1b}[48;5;{}m{:^w$}\u{1b}[0m",
skin.computing_fg(),
skin.computing_bg(),
"computing...",
w = width
)?;
} else {
clear_line(w)?;
}
Ok(())
}
pub fn draw_message(
&mut self,
w: &mut W,
y: u16,
) -> Result<()> {
let Some(message) = self.messages.first_mut() else {
return Ok(());
};
if let Some(start) = message.display_start {
if start.elapsed() > message.display_duration {
self.messages.remove(0);
return Ok(());
}
} else {
message.display_start = Some(Instant::now());
}
goto_line(w, y)?;
let markdown = format!(" {}", message.markdown);
self.status_skin.write_composite_fill(
w,
Composite::from_inline(&markdown),
self.width.into(),
Alignment::Left,
)?;
Ok(())
}
pub fn is_success(&self) -> bool {
match &self.cmd_result {
CommandResult::Report(report) => self.mission.is_success(report),
_ => false,
}
}
pub fn is_failure(&self) -> bool {
match &self.cmd_result {
CommandResult::Report(report) => !self.mission.is_success(report),
CommandResult::Failure(_) => true,
CommandResult::None => false,
}
}
pub fn prefered_scroll_end(&self) -> ScrollEnd {
match self.scroll_anchor {
ScrollAnchor::Auto => {
if self.is_failure() {
ScrollEnd::First
} else {
ScrollEnd::Last
}
}
ScrollAnchor::First => ScrollEnd::First,
ScrollAnchor::Last => ScrollEnd::Last,
}
}
fn lines_to_draw_unfiltered(&self) -> &[Line] {
if let Some(report) = self.report_to_draw() {
match (self.wrap, self.wrapped_report.as_ref()) {
(true, Some(wrapped_report)) => {
&wrapped_report.sub_lines
}
_ => {
&report.lines
}
}
} else if let Some(output) = self.cmd_result.output().or(self.output.as_ref()) {
match (self.wrap, self.wrapped_output.as_ref()) {
(true, Some(wrapped_output)) => {
&wrapped_output.sub_lines
}
_ => {
&output.lines
}
}
} else {
&[]
}
}
fn lines_to_draw(&self) -> impl Iterator<Item = &Line> {
self.lines_to_draw_unfiltered().iter().filter(|line| {
matches!(self.cmd_result, CommandResult::Failure(..)) || line.matches(self.summary)
})
}
fn report_to_draw(&self) -> Option<&Report> {
self.cmd_result
.report()
.filter(|_| !self.raw_output)
.filter(|report| !self.mission.is_success(report))
}
fn update_wrap(&mut self) {
let width = self.width - 1;
if let Some(report) = self.report_to_draw() {
if self.wrapped_report.is_none() {
self.wrapped_report = Some(WrappedReport::new(report, width));
}
} else if let Some(output) = self.cmd_result.output().or(self.output.as_ref()) {
match self.wrapped_output.as_mut() {
None => {
let old_len = self.wrapped_output.as_ref().map(|wo| wo.sub_lines.len());
let new_wrapped_output = WrappedCommandOutput::new(output, width);
let new_len = new_wrapped_output.sub_lines.len();
self.wrapped_output = Some(new_wrapped_output);
if Some(new_len) != old_len {
self.reset_scroll();
}
}
Some(wo) => {
wo.update(output, width);
}
}
}
}
pub fn draw_content(
&mut self,
w: &mut W,
y: u16,
) -> Result<()> {
if self.height < 4 {
return Ok(());
}
let skin = self.mission.job.skin;
let csi_found = format!("\u{1b}[1m\u{1b}[38;5;{}m", skin.found_fg()); let csi_found_selected = format!(
"\u{1b}[1m\u{1b}[30m\u{1b}[48;5;{}m",
skin.found_selected_bg()
); #[allow(clippy::cast_possible_truncation)]
let area = Area::new(0, y, self.width - 1, self.page_height() as u16);
let content_height = self.content_height();
let scrollbar = if self.mission.job.hide_scrollbar() {
None
} else {
area.scrollbar(self.scroll, content_height)
};
let mut top_item_idx = None;
let top = if self.reverse && self.page_height() > content_height {
self.page_height() - content_height
} else {
0
};
#[allow(clippy::cast_possible_truncation)]
let top = area.top + top as u16;
for y in area.top..top {
goto_line(w, y)?;
clear_line(w)?;
}
let width = self.width as usize;
let lines = self.lines_to_draw();
let mut lines = lines.enumerate().skip(self.scroll);
let mut found_idx = 0;
#[derive(Debug)]
struct PendingContinuation<'s> {
trange: TRange,
style: &'s str,
}
let mut pending_continuation = None;
for row_idx in 0..area.height {
let y = row_idx + top;
goto_line(w, y)?;
if let Some((line_idx, line)) = lines.next() {
top_item_idx.get_or_insert(line.item_idx);
line.line_type.draw(w, line.item_idx)?;
write!(w, " ")?;
if width > line.line_type.cols() + 1 {
let mut tline = &line.content;
let mut line_founds = Vec::new();
let found_idx_before_line = found_idx;
let founds = self.search.founds();
while found_idx < founds.len() {
let found = &founds[found_idx];
if found.line_idx > line_idx {
break;
}
if found.line_idx == line_idx {
line_founds.push(found);
}
found_idx += 1;
}
let mut modified;
let previous_continuation = pending_continuation.take();
if previous_continuation.is_some() || !line_founds.is_empty() {
modified = tline.clone();
for (in_line_idx, found) in line_founds.iter().enumerate().rev() {
let cur_idx = found_idx_before_line + in_line_idx;
let style = if self.search.selected_found() == cur_idx {
&csi_found_selected
} else {
&csi_found
};
modified.change_range_style(found.trange, style.clone());
if let Some(continued) = &found.continued {
pending_continuation = Some(PendingContinuation {
trange: *continued,
style,
});
}
}
if let Some(continuation) = previous_continuation {
modified.change_range_style(
continuation.trange,
continuation.style.to_string(),
);
}
tline = &modified;
}
tline.draw_in(w, width - 1 - line.line_type.cols())?;
}
}
clear_line(w)?;
if is_thumb(y.into(), scrollbar) {
execute!(w, cursor::MoveTo(area.width, y), Print('▐'.to_string()))?;
}
}
drop(lines);
if !self.computing {
if let Some(idx) = top_item_idx {
self.top_item_idx = idx;
}
}
Ok(())
}
pub fn draw(
&mut self,
w: &mut W,
) -> Result<()> {
self.update_search();
if self.reverse {
self.draw_status_line(w, 0)?;
if let Some(help_page) = self.help_page.as_mut() {
help_page.draw(w, Area::new(0, 1, self.width, self.height - 1))?;
} else {
self.draw_content(w, 1)?;
self.draw_computing(w, self.height - 2)?;
self.draw_message(w, self.height - 2)?;
self.draw_badges(w, self.height - 1)?;
}
} else {
if let Some(help_page) = self.help_page.as_mut() {
help_page.draw(w, Area::new(0, 0, self.width, self.height - 1))?;
} else {
self.draw_badges(w, 0)?;
self.draw_computing(w, 1)?;
self.draw_message(w, 1)?; self.draw_content(w, 2)?;
}
self.draw_status_line(w, self.height - 1)?;
}
match &mut self.dialog {
Dialog::None => {}
Dialog::Menu(menu) => {
menu.set_available_area(Area::new(0, 0, self.width, self.height));
menu.draw(w, &self.mission.job.skin)?;
}
}
w.flush()?;
Ok(())
}
}