clean up files, parse arguments with clap
This commit is contained in:
-923
@@ -1,8 +1,4 @@
|
||||
use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{style::{Color, Stylize}, text::Span};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, app::WindowSize, buffer::{Buffer, InspectionStatus, Mode, PartialAction, Popup}, cursor::Cursor, edit_action::EditAction};
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Debug)]
|
||||
@@ -559,922 +555,3 @@ impl TryFrom<&str> for CursorAction {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn execute(&mut self, action: BufferAction, window_size: WindowSize) {
|
||||
match action {
|
||||
BufferAction::NormalMode => self.normal_mode(),
|
||||
BufferAction::SelectMode => self.select_mode(),
|
||||
|
||||
BufferAction::Goto => self.goto(),
|
||||
BufferAction::View => self.view(),
|
||||
BufferAction::Replace => self.replace(),
|
||||
BufferAction::Space => self.space(),
|
||||
BufferAction::Repeat => self.repeat(),
|
||||
BufferAction::To => self.to(),
|
||||
|
||||
BufferAction::ScrollDown => self.scroll_down(window_size),
|
||||
BufferAction::ScrollUp => self.scroll_up(window_size),
|
||||
|
||||
BufferAction::PageCursorHalfDown => self.page_cursor_half_down(window_size),
|
||||
BufferAction::PageCursorHalfUp => self.page_cursor_half_up(window_size),
|
||||
|
||||
BufferAction::PageDown => self.page_down(window_size),
|
||||
BufferAction::PageUp => self.page_up(window_size),
|
||||
|
||||
BufferAction::CollapseSelection => self.collapse_selection(),
|
||||
BufferAction::FlipSelections => self.flip_selection(window_size),
|
||||
|
||||
BufferAction::Delete => self.delete(window_size),
|
||||
|
||||
BufferAction::Undo => self.undo(window_size),
|
||||
BufferAction::Redo => self.redo(window_size),
|
||||
|
||||
BufferAction::Save => self.save(),
|
||||
|
||||
BufferAction::CopySelectionOnNextLine => self.copy_selection_on_next_line(window_size),
|
||||
|
||||
BufferAction::RotateSelectionsBackward => self.rotate_selections_backward(window_size),
|
||||
BufferAction::RotateSelectionsForward => self.rotate_selections_forward(window_size),
|
||||
|
||||
BufferAction::KeepPrimarySelection => self.keep_primary_selection(),
|
||||
BufferAction::RemovePrimarySelection => self.remove_primary_selection(),
|
||||
|
||||
BufferAction::SplitSelectionsInto1s => self.split_selections_into_size(1, window_size),
|
||||
BufferAction::SplitSelectionsInto2s => self.split_selections_into_size(2, window_size),
|
||||
BufferAction::SplitSelectionsInto3s => self.split_selections_into_size(3, window_size),
|
||||
BufferAction::SplitSelectionsInto4s => self.split_selections_into_size(4, window_size),
|
||||
BufferAction::SplitSelectionsInto5s => self.split_selections_into_size(5, window_size),
|
||||
BufferAction::SplitSelectionsInto6s => self.split_selections_into_size(6, window_size),
|
||||
BufferAction::SplitSelectionsInto7s => self.split_selections_into_size(7, window_size),
|
||||
BufferAction::SplitSelectionsInto8s => self.split_selections_into_size(8, window_size),
|
||||
BufferAction::SplitSelectionsInto9s => self.split_selections_into_size(9, window_size),
|
||||
|
||||
BufferAction::JumpToSelectedOffset => self.jump_to_selected_offset(window_size),
|
||||
BufferAction::JumpToSelectedOffsetRelativeToMark => self.jump_to_selected_offset_relative_to_mark(window_size),
|
||||
|
||||
BufferAction::ToggleMark => self.toggle_mark(),
|
||||
|
||||
BufferAction::AlignViewCenter => self.align_view_center(window_size),
|
||||
BufferAction::AlignViewBottom => self.align_view_bottom(window_size),
|
||||
BufferAction::AlignViewTop => self.align_view_top(),
|
||||
|
||||
BufferAction::ExtendToMark => self.extend_to_mark(window_size),
|
||||
BufferAction::ExtendToNull => self.extend_to_null(window_size),
|
||||
BufferAction::ExtendToFF => self.extend_to_FF(window_size),
|
||||
|
||||
BufferAction::InspectSelection => self.inspect_selection(),
|
||||
BufferAction::InspectSelectionColor => self.inspect_selection_color(),
|
||||
}
|
||||
}
|
||||
|
||||
const fn normal_mode(&mut self) {
|
||||
self.mode = Mode::Normal;
|
||||
}
|
||||
|
||||
const fn select_mode(&mut self) {
|
||||
self.mode = Mode::Select;
|
||||
}
|
||||
|
||||
const fn goto(&mut self) {
|
||||
self.partial_action = Some(PartialAction::Goto);
|
||||
}
|
||||
|
||||
const fn view(&mut self) {
|
||||
self.partial_action = Some(PartialAction::View);
|
||||
}
|
||||
|
||||
const fn replace(&mut self) {
|
||||
if !self.contents.is_empty() {
|
||||
self.partial_action = Some(PartialAction::Replace);
|
||||
}
|
||||
}
|
||||
|
||||
const fn space(&mut self) {
|
||||
self.partial_action = Some(PartialAction::Space);
|
||||
}
|
||||
|
||||
const fn repeat(&mut self) {
|
||||
self.partial_action = Some(PartialAction::Repeat);
|
||||
}
|
||||
|
||||
const fn to(&mut self) {
|
||||
self.partial_action = Some(PartialAction::To);
|
||||
}
|
||||
|
||||
pub fn scroll_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= BYTES_OF_PADDING { return; }
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + BYTES_PER_LINE,
|
||||
self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE
|
||||
);
|
||||
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
pub fn scroll_up(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE);
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_cursor_half_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= BYTES_OF_PADDING { return; }
|
||||
|
||||
let old_scroll_position = self.scroll_position;
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE),
|
||||
self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE
|
||||
);
|
||||
|
||||
let scroll_position_change = self.scroll_position - old_scroll_position;
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.head = min(
|
||||
self.primary_cursor.head + scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
self.primary_cursor.tail = min(
|
||||
self.primary_cursor.tail + scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.head = (cursor.head + scroll_position_change).min(max_contents_index);
|
||||
cursor.tail = (cursor.tail + scroll_position_change).min(max_contents_index);
|
||||
}
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_cursor_half_up(&mut self, window_size: WindowSize) {
|
||||
let old_scroll_position = self.scroll_position;
|
||||
|
||||
self.scroll_position = self.scroll_position.saturating_sub(
|
||||
(window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE)
|
||||
);
|
||||
|
||||
let scroll_position_change = old_scroll_position - self.scroll_position;
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.head = min(
|
||||
self.primary_cursor.head - scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
self.primary_cursor.tail = min(
|
||||
self.primary_cursor.tail - scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.head = (cursor.head - scroll_position_change).min(max_contents_index);
|
||||
cursor.tail = (cursor.tail - scroll_position_change).min(max_contents_index);
|
||||
}
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= BYTES_OF_PADDING { return; }
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + window_size.visible_byte_count(),
|
||||
self.max_contents_index() - BYTES_OF_PADDING - self.max_contents_index() % BYTES_PER_LINE
|
||||
);
|
||||
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_up(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.scroll_position.saturating_sub(
|
||||
window_size.visible_byte_count()
|
||||
);
|
||||
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn collapse_selection(&mut self) {
|
||||
self.primary_cursor.collapse();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
fn flip_selection(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.flip();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.flip();
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn delete(&mut self, window_size: WindowSize) {
|
||||
if !self.contents.is_empty() {
|
||||
self.execute_and_add(
|
||||
EditAction::Delete {
|
||||
primary_cursor: self.primary_cursor,
|
||||
cursors: self.cursors.clone(),
|
||||
primary_old_data: self.contents[self.primary_cursor.range()].into(),
|
||||
old_data: self.cursors
|
||||
.iter()
|
||||
.map(|cursor| self.contents[cursor.range()].to_vec())
|
||||
.collect(),
|
||||
},
|
||||
window_size
|
||||
);
|
||||
}
|
||||
|
||||
if self.mode == Mode::Select {
|
||||
self.mode = Mode::Normal;
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(&mut self, window_size: WindowSize) {
|
||||
if self.time_traveling == Some(0) || self.edit_history.is_empty() { return; }
|
||||
|
||||
let current_date = self.time_traveling
|
||||
.map_or(self.edit_history.len() - 1, |date| date - 1);
|
||||
|
||||
self.time_traveling = Some(current_date);
|
||||
|
||||
let edit_action = replace(
|
||||
&mut self.edit_history[current_date],
|
||||
EditAction::Placeholder
|
||||
);
|
||||
|
||||
self.undo_edit(&edit_action, window_size);
|
||||
|
||||
self.edit_history[current_date] = edit_action;
|
||||
}
|
||||
|
||||
fn redo(&mut self, window_size: WindowSize) {
|
||||
let Some(previous_date) = self.time_traveling else { return; };
|
||||
|
||||
let current_date = previous_date + 1;
|
||||
|
||||
self.time_traveling = if current_date == self.edit_history.len() {
|
||||
None
|
||||
} else {
|
||||
Some(current_date)
|
||||
};
|
||||
|
||||
let edit_action = replace(
|
||||
&mut self.edit_history[previous_date],
|
||||
EditAction::Placeholder
|
||||
);
|
||||
|
||||
self.execute_edit(&edit_action, window_size);
|
||||
|
||||
self.edit_history[previous_date] = edit_action;
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
let mut file = File::create(&self.file_path).unwrap();
|
||||
file.write_all(&self.contents).unwrap();
|
||||
|
||||
self.last_saved_at = Some(
|
||||
self.time_traveling.unwrap_or(self.edit_history.len())
|
||||
);
|
||||
}
|
||||
|
||||
fn copy_selection_on_next_line(&mut self, window_size: WindowSize) {
|
||||
let new_cursors: Vec<Cursor> = iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.filter_map(|cursor| {
|
||||
let number_of_lines_tall = (cursor.upper_bound() - cursor.lower_bound()) / BYTES_PER_LINE;
|
||||
let offset_to_add = (number_of_lines_tall + 1) * BYTES_PER_LINE;
|
||||
|
||||
if cursor.lower_bound() + offset_to_add < self.contents.len() {
|
||||
Some(
|
||||
Cursor {
|
||||
head: min(cursor.head + offset_to_add, self.max_contents_index()),
|
||||
tail: min(cursor.tail + offset_to_add, self.max_contents_index())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.cursors.extend(new_cursors);
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
|
||||
self.rotate_selections_forward(window_size);
|
||||
}
|
||||
|
||||
fn rotate_selections_backward(&mut self, window_size: WindowSize) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let next_cursor_index = self.cursors
|
||||
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
|
||||
if next_cursor_index == 0 {
|
||||
let cursor_count = self.cursors.len();
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[cursor_count - 1]);
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
} else {
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index - 1]);
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn rotate_selections_forward(&mut self, window_size: WindowSize) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let next_cursor_index = self.cursors
|
||||
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
if next_cursor_index == self.cursors.len() {
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[0]);
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
} else {
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index]);
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn keep_primary_selection(&mut self) {
|
||||
self.cursors.clear();
|
||||
}
|
||||
|
||||
fn remove_primary_selection(&mut self) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let next_cursor_index = self.cursors
|
||||
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
if next_cursor_index == self.cursors.len() {
|
||||
self.primary_cursor = self.cursors.remove(0);
|
||||
} else {
|
||||
self.primary_cursor = self.cursors.remove(next_cursor_index);
|
||||
}
|
||||
}
|
||||
|
||||
fn split_selections_into_size(&mut self, size: usize, window_size: WindowSize) {
|
||||
if !iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.all(|cursor| cursor.len().is_multiple_of(size))
|
||||
{
|
||||
self.alert_message = Span::from(
|
||||
format!("not all selections are a multiple of {size} long")
|
||||
).red();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new_cursors = iter::once(self.primary_cursor)
|
||||
.chain(self.cursors.iter().copied())
|
||||
.flat_map(|cursor| {
|
||||
cursor
|
||||
.range()
|
||||
.step_by(size)
|
||||
.map(|tail| Cursor { head: tail + size - 1, tail })
|
||||
});
|
||||
|
||||
self.primary_cursor = new_cursors.next().unwrap();
|
||||
self.cursors = new_cursors.collect();
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn jump_to_selected_offset(&mut self, window_size: WindowSize) {
|
||||
if !iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.all(|cursor| {
|
||||
bytes_to_nat(&self.contents[cursor.range()])
|
||||
.map(|nat| nat as usize)
|
||||
.is_some_and(|offset| offset < self.contents.len())
|
||||
})
|
||||
{
|
||||
if self.cursors.is_empty() {
|
||||
self.alert_message = Span::from(
|
||||
"selection is not a valid offset"
|
||||
).red();
|
||||
} else {
|
||||
self.alert_message = Span::from(
|
||||
"not all selections are valid offsets"
|
||||
).red();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.primary_cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[self.primary_cursor.range()]).unwrap() as usize
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
*cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[cursor.range()]).unwrap() as usize
|
||||
);
|
||||
}
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn jump_to_selected_offset_relative_to_mark(&mut self, window_size: WindowSize) {
|
||||
let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect();
|
||||
sorted_marks.sort_unstable();
|
||||
|
||||
if !iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.all(|cursor| {
|
||||
bytes_to_nat(&self.contents[cursor.range()])
|
||||
.map(|offset| mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize)
|
||||
.is_some_and(|offset| offset < self.contents.len())
|
||||
})
|
||||
{
|
||||
if self.cursors.is_empty() {
|
||||
self.alert_message = Span::from(
|
||||
"selection is not a valid offset"
|
||||
).red();
|
||||
} else {
|
||||
self.alert_message = Span::from(
|
||||
"not all selections are valid offsets"
|
||||
).red();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.primary_cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[self.primary_cursor.range()])
|
||||
.map(|offset| {
|
||||
mark_before(self.primary_cursor.lower_bound(), &sorted_marks) + offset as usize
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
*cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[cursor.range()])
|
||||
.map(|offset| {
|
||||
mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn toggle_mark(&mut self) {
|
||||
match self.marks.entry(self.primary_cursor.lower_bound()) {
|
||||
Entry::Occupied(occupied_entry) => { occupied_entry.remove(); },
|
||||
Entry::Vacant(vacant_entry) => vacant_entry.insert(),
|
||||
}
|
||||
|
||||
for cursor in &self.cursors {
|
||||
match self.marks.entry(cursor.lower_bound()) {
|
||||
Entry::Occupied(occupied_entry) => { occupied_entry.remove(); },
|
||||
Entry::Vacant(vacant_entry) => vacant_entry.insert(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn align_view_center(&mut self, window_size: WindowSize) {
|
||||
let half_a_screen = window_size.visible_byte_count() / 2;
|
||||
|
||||
self.scroll_position = self.primary_cursor.head
|
||||
.saturating_sub(self.primary_cursor.head % BYTES_PER_LINE)
|
||||
.saturating_sub(half_a_screen - (half_a_screen % BYTES_PER_LINE));
|
||||
}
|
||||
|
||||
fn align_view_bottom(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.primary_cursor.head
|
||||
.saturating_sub(self.primary_cursor.head % BYTES_PER_LINE)
|
||||
.saturating_sub(
|
||||
window_size
|
||||
.visible_byte_count()
|
||||
.saturating_sub(BYTES_PER_LINE + BYTES_OF_PADDING)
|
||||
)
|
||||
.min(self.max_contents_index() - self.max_contents_index() % BYTES_PER_LINE);
|
||||
}
|
||||
|
||||
const fn align_view_top(&mut self) {
|
||||
self.scroll_position = self.primary_cursor.head
|
||||
.saturating_sub(self.primary_cursor.head % BYTES_PER_LINE)
|
||||
.saturating_sub(BYTES_OF_PADDING);
|
||||
}
|
||||
|
||||
fn extend_to_mark(&mut self, window_size: WindowSize) {
|
||||
let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect();
|
||||
sorted_marks.sort_unstable();
|
||||
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
let mark_after_primary = mark_after(
|
||||
self.primary_cursor.head,
|
||||
&sorted_marks,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
self.primary_cursor.tail = self.primary_cursor.head;
|
||||
self.primary_cursor.head = mark_after_primary - 1;
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
let mark_after_cursor = mark_after(
|
||||
cursor.head,
|
||||
&sorted_marks,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
cursor.tail = cursor.head;
|
||||
cursor.head = mark_after_cursor - 1;
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn extend_to_null(&mut self, window_size: WindowSize) {
|
||||
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0)
|
||||
{
|
||||
self.primary_cursor.tail = self.primary_cursor.head;
|
||||
self.primary_cursor.head += null_offset_after_primary;
|
||||
}
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
if let Some(null_offset_after_primary) = self.contents[cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0)
|
||||
{
|
||||
cursor.tail = cursor.head;
|
||||
cursor.head += null_offset_after_primary;
|
||||
}
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn extend_to_FF(&mut self, window_size: WindowSize) {
|
||||
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0xFF)
|
||||
{
|
||||
self.primary_cursor.tail = self.primary_cursor.head;
|
||||
self.primary_cursor.head += null_offset_after_primary;
|
||||
}
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
if let Some(null_offset_after_primary) = self.contents[cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0xFF)
|
||||
{
|
||||
cursor.tail = cursor.head;
|
||||
cursor.head += null_offset_after_primary;
|
||||
}
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn inspect_selection(&mut self) {
|
||||
if self.inspection_status == Some(InspectionStatus::Normal) {
|
||||
self.inspection_status = None;
|
||||
return;
|
||||
}
|
||||
|
||||
self.inspection_status = Some(InspectionStatus::Normal);
|
||||
|
||||
self.popups.extend(
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.filter_map(|cursor| {
|
||||
let selection = &self.contents[cursor.range()];
|
||||
|
||||
let popup_lines = inspect(selection);
|
||||
|
||||
if popup_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Popup::new(cursor.lower_bound(), popup_lines))
|
||||
}
|
||||
})
|
||||
.sorted_unstable_by_key(|popup| popup.at)
|
||||
);
|
||||
|
||||
if self.popups.is_empty() {
|
||||
self.inspection_status = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_selection_color(&mut self) {
|
||||
if self.inspection_status == Some(InspectionStatus::ColorsOnly) {
|
||||
self.inspection_status = None;
|
||||
return;
|
||||
}
|
||||
|
||||
self.inspection_status = Some(InspectionStatus::ColorsOnly);
|
||||
|
||||
self.popups.extend(
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.filter_map(|cursor| {
|
||||
let selection = &self.contents[cursor.range()];
|
||||
|
||||
let popup_lines = inspect_color(selection);
|
||||
|
||||
if popup_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Popup::new(cursor.lower_bound(), popup_lines))
|
||||
}
|
||||
})
|
||||
.sorted_unstable_by_key(|popup| popup.at)
|
||||
);
|
||||
|
||||
if self.popups.is_empty() {
|
||||
self.inspection_status = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect(selection: &[u8]) -> Vec<Span<'static>> {
|
||||
let nat = bytes_to_nat(selection);
|
||||
|
||||
let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len()));
|
||||
|
||||
let utf8 = str::from_utf8(selection).ok()
|
||||
.filter(|_| selection.len() == 1)
|
||||
.map(|utf8| utf8.trim_suffix('\0'))
|
||||
.filter(|utf8| !utf8.contains(is_illegal_control_character))
|
||||
.map(|utf8| Span::from(format!("\"{utf8}\"")).red());
|
||||
|
||||
let fixedpoint2012 = nat
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|nat| f64::from(nat as u32) / f64::from(1 << 12))
|
||||
.map(|fixedpoint2012| {
|
||||
let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint2012_signed = int
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|int| f64::from(int as i32) / f64::from(1 << 12))
|
||||
.map(|fixedpoint2012_signed| {
|
||||
let two_decimals_is_enough = (fixedpoint2012_signed * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("i20.12: {approximate_symbol}{fixedpoint2012_signed:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint1616 = nat
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|nat| f64::from(nat as u32) / f64::from(1 << 16))
|
||||
.map(|fixedpoint1616| {
|
||||
let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint1616_signed = int
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|int| f64::from(int as i32) / f64::from(1 << 16))
|
||||
.map(|fixedpoint1616_signed| {
|
||||
let two_decimals_is_enough = (fixedpoint1616_signed * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("i16.16: {approximate_symbol}{fixedpoint1616_signed:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint124 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.map(|nat| f64::from(nat as u16) / f64::from(1 << 4))
|
||||
.map(|fixedpoint124| {
|
||||
let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint88 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.map(|nat| f64::from(nat as u16) / f64::from(1 << 8))
|
||||
.map(|fixedpoint88| {
|
||||
let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint412 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.map(|nat| f64::from(nat as u16) / f64::from(1 << 12))
|
||||
.map(|fixedpoint412| {
|
||||
let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into()
|
||||
});
|
||||
|
||||
let color888 = (selection.len() == 3)
|
||||
.then(|| [selection[0], selection[1], selection[2]])
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("#{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
let color555 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.filter(|&nat| nat >> 15 == 0)
|
||||
.map(|nat| color555_to_color888(nat as u16))
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("555: #{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
int.map(|int| format!("{int}").into())
|
||||
.into_iter()
|
||||
.chain(nat.map(|nat| format!("{nat}").into()))
|
||||
.chain(utf8)
|
||||
.chain(fixedpoint2012_signed)
|
||||
.chain(fixedpoint2012)
|
||||
.chain(fixedpoint1616_signed)
|
||||
.chain(fixedpoint1616)
|
||||
.chain(fixedpoint124)
|
||||
.chain(fixedpoint88)
|
||||
.chain(fixedpoint412)
|
||||
.chain(color888)
|
||||
.chain(color555)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn inspect_color(selection: &[u8]) -> Vec<Span<'static>> {
|
||||
let nat = bytes_to_nat(selection);
|
||||
|
||||
let color888 = (selection.len() == 3)
|
||||
.then(|| [selection[0], selection[1], selection[2]])
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("#{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
let color555 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.filter(|&nat| nat >> 15 == 0)
|
||||
.map(|nat| color555_to_color888(nat as u16))
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("#{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
color888
|
||||
.into_iter()
|
||||
.chain(color555)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// helpers
|
||||
impl Buffer {
|
||||
pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) {
|
||||
if self.primary_cursor.head < self.scroll_position + BYTES_OF_PADDING {
|
||||
self.align_view_top();
|
||||
} else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(BYTES_OF_PADDING) {
|
||||
self.align_view_bottom(window_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_to_nat(bytes: &[u8]) -> Option<u64> {
|
||||
bytes
|
||||
.iter()
|
||||
.rev() // little-endian
|
||||
.skip_while(|&&byte| byte == 0)
|
||||
.try_fold(u64::default(), |result, &byte| {
|
||||
Some(result.shl_exact(8)? | u64::from(byte))
|
||||
})
|
||||
}
|
||||
|
||||
const fn nat_to_int_if_different(nat: u64, bytes: usize) -> Option<i64> {
|
||||
match bytes {
|
||||
1 if nat > i8::MAX as u64 => Some((nat as u8).cast_signed() as i64),
|
||||
2 if nat > i16::MAX as u64 => Some((nat as u16).cast_signed() as i64),
|
||||
4 if nat > i32::MAX as u64 => Some((nat as u32).cast_signed() as i64),
|
||||
8 if nat > i64::MAX as u64 => Some(nat.cast_signed()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_to_int_tests() {
|
||||
assert_eq!(nat_to_int_if_different(0, 1), None);
|
||||
assert_eq!(nat_to_int_if_different(i8::MAX as u64, 1), None);
|
||||
assert_eq!(nat_to_int_if_different(i8::MAX as u64 + 1, 1), Some(i8::MIN.into()));
|
||||
assert_eq!(nat_to_int_if_different(u8::MAX.into(), 1), Some(-1));
|
||||
|
||||
assert_eq!(nat_to_int_if_different(0, 2), None);
|
||||
assert_eq!(nat_to_int_if_different(i16::MAX as u64, 2), None);
|
||||
assert_eq!(nat_to_int_if_different(i16::MAX as u64 + 1, 2), Some(i16::MIN.into()));
|
||||
assert_eq!(nat_to_int_if_different(u16::MAX.into(), 2), Some(-1));
|
||||
}
|
||||
|
||||
// or 0 if no mark is before
|
||||
fn mark_before(offset: usize, sorted_marks: &[usize]) -> usize {
|
||||
match sorted_marks.binary_search(&offset) {
|
||||
Ok(_) => offset,
|
||||
Err(0) => 0,
|
||||
Err(mark_after_index) => sorted_marks[mark_after_index - 1],
|
||||
}
|
||||
}
|
||||
|
||||
// or end index if no mark is after
|
||||
fn mark_after(offset: usize, sorted_marks: &[usize], max: usize) -> usize {
|
||||
if sorted_marks.is_empty() { return max + 1; }
|
||||
|
||||
match sorted_marks.binary_search(&offset) {
|
||||
Ok(mark_before_index) => if mark_before_index == sorted_marks.len() - 1 {
|
||||
max + 1
|
||||
} else {
|
||||
sorted_marks[mark_before_index + 1]
|
||||
},
|
||||
Err(mark_after_index) => {
|
||||
if mark_after_index == sorted_marks.len() {
|
||||
max + 1
|
||||
} else {
|
||||
sorted_marks[mark_after_index]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_illegal_control_character(character: char) -> bool {
|
||||
match character {
|
||||
'\t' | '\n' | '\r' => false,
|
||||
_ if character.is_ascii_control() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
const fn color555_to_color888(color555: u16) -> [u8; 3] {
|
||||
[
|
||||
// 8 is the ratio between the number of colors in 555 vs 888 (32:256)
|
||||
(color555 & 0b11111) as u8 * 8,
|
||||
(color555 >> 5 & 0b11111) as u8 * 8,
|
||||
(color555 >> 10 & 0b11111) as u8 * 8
|
||||
]
|
||||
}
|
||||
|
||||
+14
-74
@@ -1,11 +1,10 @@
|
||||
use std::{env, io, path::PathBuf, process::exit, time::Duration};
|
||||
use std::{io, path::PathBuf, process::exit, time::Duration};
|
||||
use crossterm::{ExecutableCommand, event::{self, DisableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, terminal::window_size};
|
||||
use ratatui::{DefaultTerminal, style::Stylize, text::Span};
|
||||
use crate::{BYTES_PER_LINE, action::AppAction, buffer::Buffer, config::{Config, ConfigInitError}, cursor::Cursor};
|
||||
use crate::{BYTES_PER_LINE, action::AppAction, buffer::Buffer, config::{Config, ConfigInitError}, cursor::Cursor, window_size::WindowSize};
|
||||
|
||||
mod widget;
|
||||
|
||||
const MACOS_STDIN_BROKEN_MESSAGE: &str = "reading from stdin on macOS does not work due to a limitation in crossterm. see https://github.com/crossterm-rs/crossterm/issues/396";
|
||||
mod actions;
|
||||
|
||||
pub struct App {
|
||||
pub config: Config,
|
||||
@@ -23,16 +22,13 @@ pub struct App {
|
||||
pub logs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct WindowSize {
|
||||
pub rows: usize,
|
||||
pub covered_rows: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(
|
||||
config_path: Option<PathBuf>,
|
||||
files: &[PathBuf],
|
||||
) -> Self {
|
||||
let config = {
|
||||
let config = Config::init();
|
||||
let config = Config::init(config_path);
|
||||
|
||||
match &config {
|
||||
Err(ConfigInitError::IO(io_error)) if io_error.kind() != io::ErrorKind::NotFound => {
|
||||
@@ -56,10 +52,9 @@ impl App {
|
||||
|
||||
let mut error_alert: Option<Span> = None;
|
||||
|
||||
let mut buffers: Vec<Buffer> = env::args()
|
||||
.skip(1)
|
||||
.map(Into::into)
|
||||
.filter_map(|path: PathBuf| {
|
||||
let mut buffers: Vec<Buffer> = files
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
Buffer::from_file_at(path.clone())
|
||||
.inspect_err(|error| {
|
||||
error_alert = Some(
|
||||
@@ -70,9 +65,9 @@ impl App {
|
||||
})
|
||||
.collect();
|
||||
|
||||
if env::args().len() <= 1 {
|
||||
if files.is_empty() {
|
||||
#[cfg(target_os = "macos")] {
|
||||
eprintln!("{MACOS_STDIN_BROKEN_MESSAGE}");
|
||||
eprintln!("please provide at least one file as input. use --help for options");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -137,7 +132,7 @@ impl App {
|
||||
if error.kind() == ErrorKind::Other {
|
||||
let error_message = error.to_string();
|
||||
if error_message == "Failed to initialize input reader" {
|
||||
eprintln!("{MACOS_STDIN_BROKEN_MESSAGE}");
|
||||
eprintln!("reading from stdin on macOS does not work due to a limitation in crossterm. see https://github.com/crossterm-rs/crossterm/issues/396");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -244,59 +239,4 @@ impl App {
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn quit_if_saved(&mut self) {
|
||||
if self.buffers.iter().all(Buffer::all_changes_saved) {
|
||||
self.quit();
|
||||
} else {
|
||||
self.buffers[self.current_buffer_index].alert_message = Span::from(
|
||||
"there are unsaved changes, use Q to override"
|
||||
).red();
|
||||
}
|
||||
}
|
||||
|
||||
const fn quit(&mut self) {
|
||||
self.should_quit = true;
|
||||
}
|
||||
|
||||
const fn previous_buffer(&mut self) {
|
||||
if self.current_buffer_index == 0 {
|
||||
self.current_buffer_index = self.buffers.len() - 1;
|
||||
} else {
|
||||
self.current_buffer_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
const fn next_buffer(&mut self) {
|
||||
if self.current_buffer_index == self.buffers.len() - 1 {
|
||||
self.current_buffer_index = 0;
|
||||
} else {
|
||||
self.current_buffer_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn yank(&mut self) {
|
||||
let current_buffer = &self.buffers[self.current_buffer_index];
|
||||
|
||||
self.primary_cursor_register = current_buffer
|
||||
.contents[current_buffer.primary_cursor.range()]
|
||||
.to_vec();
|
||||
|
||||
self.other_cursor_registers = current_buffer.cursors
|
||||
.iter()
|
||||
.map(|cursor| {
|
||||
current_buffer.contents[cursor.range()].to_vec()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowSize {
|
||||
pub const fn visible_byte_count(&self) -> usize {
|
||||
self.hex_rows() * BYTES_PER_LINE
|
||||
}
|
||||
|
||||
pub const fn hex_rows(&self) -> usize {
|
||||
self.rows - self.covered_rows
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use ratatui::{style::Stylize, text::Span};
|
||||
|
||||
use crate::{app::App, buffer::Buffer};
|
||||
|
||||
impl App {
|
||||
pub fn quit_if_saved(&mut self) {
|
||||
if self.buffers.iter().all(Buffer::all_changes_saved) {
|
||||
self.quit();
|
||||
} else {
|
||||
self.buffers[self.current_buffer_index].alert_message = Span::from(
|
||||
"there are unsaved changes, use Q to override"
|
||||
).red();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn quit(&mut self) {
|
||||
self.should_quit = true;
|
||||
}
|
||||
|
||||
pub const fn previous_buffer(&mut self) {
|
||||
if self.current_buffer_index == 0 {
|
||||
self.current_buffer_index = self.buffers.len() - 1;
|
||||
} else {
|
||||
self.current_buffer_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn next_buffer(&mut self) {
|
||||
if self.current_buffer_index == self.buffers.len() - 1 {
|
||||
self.current_buffer_index = 0;
|
||||
} else {
|
||||
self.current_buffer_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn yank(&mut self) {
|
||||
let current_buffer = &self.buffers[self.current_buffer_index];
|
||||
|
||||
self.primary_cursor_register = current_buffer
|
||||
.contents[current_buffer.primary_cursor.range()]
|
||||
.to_vec();
|
||||
|
||||
self.other_cursor_registers = current_buffer.cursors
|
||||
.iter()
|
||||
.map(|cursor| {
|
||||
current_buffer.contents[cursor.range()].to_vec()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use std::path::PathBuf;
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about)]
|
||||
pub struct Arguments {
|
||||
/// specify a file to use for configuration
|
||||
#[arg(short, long, value_name = "file")]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// the input files to edit
|
||||
#[arg(value_name = "files")]
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
use core::slice::GetDisjointMutIndex;
|
||||
use std::{collections::HashSet, fs::File, io::{self, Read}, path::PathBuf};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{layout::{Constraint, Rect}, style::{Color, Style, Stylize}, text::Span, widgets::{Block, Borders, Clear, Widget}};
|
||||
use ratatui::{style::Stylize, text::Span};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::{BYTES_PER_LINE, action::{Action, AppAction, bytes_to_nat}, app::WindowSize, config::Config, cursor::Cursor, edit_action::EditAction};
|
||||
use crate::{BYTES_PER_LINE, action::{Action, AppAction}, buffer::actions::bytes_to_nat, config::Config, cursor::Cursor, edit_action::EditAction, popup::Popup, window_size::WindowSize};
|
||||
|
||||
mod widget;
|
||||
mod actions;
|
||||
|
||||
pub struct Buffer {
|
||||
pub file_name: String,
|
||||
@@ -51,52 +52,11 @@ pub enum PartialAction {
|
||||
Goto, View, Replace, Space, Repeat, To
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Popup {
|
||||
pub at: usize,
|
||||
width: u16,
|
||||
primary: bool,
|
||||
lines: Vec<Span<'static>>
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InspectionStatus {
|
||||
Normal, ColorsOnly
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => " NORMAL ",
|
||||
Self::Select => " SELECT ",
|
||||
Self::Insert => " INSERT ",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn color(self) -> Color {
|
||||
match self {
|
||||
Self::Normal => Color::Blue,
|
||||
Self::Select => Color::Yellow,
|
||||
Self::Insert => Color::Green,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialAction {
|
||||
pub const fn label(self) -> &'static str {
|
||||
use PartialAction::*;
|
||||
|
||||
match self {
|
||||
Goto => "g",
|
||||
View => "z",
|
||||
Replace => "r",
|
||||
Space => "␠",
|
||||
Repeat => "×",
|
||||
To => "t",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PartialAction {
|
||||
type Error = ();
|
||||
|
||||
@@ -115,61 +75,6 @@ impl TryFrom<&str> for PartialAction {
|
||||
}
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
pub fn new(at: usize, lines: Vec<Span<'static>>) -> Self {
|
||||
Self {
|
||||
at,
|
||||
width: lines
|
||||
.iter()
|
||||
.map(|line| line.width() as u16)
|
||||
.max()
|
||||
.unwrap_or(0),
|
||||
primary: false,
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
const fn area_at(&self, x: u16, y: u16) -> Rect {
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: self.width + 2,
|
||||
height: self.lines.len() as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
const fn as_primary(mut self) -> Self {
|
||||
self.primary = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Popup {
|
||||
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let border_color = if self.primary {
|
||||
Style::new().white()
|
||||
} else {
|
||||
Style::new().gray()
|
||||
};
|
||||
|
||||
Block::new()
|
||||
.on_dark_gray()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_style(border_color)
|
||||
.render(area, buf);
|
||||
|
||||
for (line, area) in self.lines.iter().zip(area.rows()) {
|
||||
line.render(
|
||||
area.centered_horizontally(Constraint::Length(line.width() as u16)),
|
||||
buf
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn from_file_at(file_path: PathBuf) -> io::Result<Self> {
|
||||
let mut file = File::open(&file_path)?;
|
||||
@@ -0,0 +1,923 @@
|
||||
use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{style::{Color, Stylize}, text::Span};
|
||||
use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, action::BufferAction, buffer::{Buffer, InspectionStatus, Mode, PartialAction}, cursor::Cursor, edit_action::EditAction, popup::Popup, window_size::WindowSize};
|
||||
|
||||
impl Buffer {
|
||||
pub fn execute(&mut self, action: BufferAction, window_size: WindowSize) {
|
||||
match action {
|
||||
BufferAction::NormalMode => self.normal_mode(),
|
||||
BufferAction::SelectMode => self.select_mode(),
|
||||
|
||||
BufferAction::Goto => self.goto(),
|
||||
BufferAction::View => self.view(),
|
||||
BufferAction::Replace => self.replace(),
|
||||
BufferAction::Space => self.space(),
|
||||
BufferAction::Repeat => self.repeat(),
|
||||
BufferAction::To => self.to(),
|
||||
|
||||
BufferAction::ScrollDown => self.scroll_down(window_size),
|
||||
BufferAction::ScrollUp => self.scroll_up(window_size),
|
||||
|
||||
BufferAction::PageCursorHalfDown => self.page_cursor_half_down(window_size),
|
||||
BufferAction::PageCursorHalfUp => self.page_cursor_half_up(window_size),
|
||||
|
||||
BufferAction::PageDown => self.page_down(window_size),
|
||||
BufferAction::PageUp => self.page_up(window_size),
|
||||
|
||||
BufferAction::CollapseSelection => self.collapse_selection(),
|
||||
BufferAction::FlipSelections => self.flip_selection(window_size),
|
||||
|
||||
BufferAction::Delete => self.delete(window_size),
|
||||
|
||||
BufferAction::Undo => self.undo(window_size),
|
||||
BufferAction::Redo => self.redo(window_size),
|
||||
|
||||
BufferAction::Save => self.save(),
|
||||
|
||||
BufferAction::CopySelectionOnNextLine => self.copy_selection_on_next_line(window_size),
|
||||
|
||||
BufferAction::RotateSelectionsBackward => self.rotate_selections_backward(window_size),
|
||||
BufferAction::RotateSelectionsForward => self.rotate_selections_forward(window_size),
|
||||
|
||||
BufferAction::KeepPrimarySelection => self.keep_primary_selection(),
|
||||
BufferAction::RemovePrimarySelection => self.remove_primary_selection(),
|
||||
|
||||
BufferAction::SplitSelectionsInto1s => self.split_selections_into_size(1, window_size),
|
||||
BufferAction::SplitSelectionsInto2s => self.split_selections_into_size(2, window_size),
|
||||
BufferAction::SplitSelectionsInto3s => self.split_selections_into_size(3, window_size),
|
||||
BufferAction::SplitSelectionsInto4s => self.split_selections_into_size(4, window_size),
|
||||
BufferAction::SplitSelectionsInto5s => self.split_selections_into_size(5, window_size),
|
||||
BufferAction::SplitSelectionsInto6s => self.split_selections_into_size(6, window_size),
|
||||
BufferAction::SplitSelectionsInto7s => self.split_selections_into_size(7, window_size),
|
||||
BufferAction::SplitSelectionsInto8s => self.split_selections_into_size(8, window_size),
|
||||
BufferAction::SplitSelectionsInto9s => self.split_selections_into_size(9, window_size),
|
||||
|
||||
BufferAction::JumpToSelectedOffset => self.jump_to_selected_offset(window_size),
|
||||
BufferAction::JumpToSelectedOffsetRelativeToMark => self.jump_to_selected_offset_relative_to_mark(window_size),
|
||||
|
||||
BufferAction::ToggleMark => self.toggle_mark(),
|
||||
|
||||
BufferAction::AlignViewCenter => self.align_view_center(window_size),
|
||||
BufferAction::AlignViewBottom => self.align_view_bottom(window_size),
|
||||
BufferAction::AlignViewTop => self.align_view_top(),
|
||||
|
||||
BufferAction::ExtendToMark => self.extend_to_mark(window_size),
|
||||
BufferAction::ExtendToNull => self.extend_to_null(window_size),
|
||||
BufferAction::ExtendToFF => self.extend_to_FF(window_size),
|
||||
|
||||
BufferAction::InspectSelection => self.inspect_selection(),
|
||||
BufferAction::InspectSelectionColor => self.inspect_selection_color(),
|
||||
}
|
||||
}
|
||||
|
||||
const fn normal_mode(&mut self) {
|
||||
self.mode = Mode::Normal;
|
||||
}
|
||||
|
||||
const fn select_mode(&mut self) {
|
||||
self.mode = Mode::Select;
|
||||
}
|
||||
|
||||
const fn goto(&mut self) {
|
||||
self.partial_action = Some(PartialAction::Goto);
|
||||
}
|
||||
|
||||
const fn view(&mut self) {
|
||||
self.partial_action = Some(PartialAction::View);
|
||||
}
|
||||
|
||||
const fn replace(&mut self) {
|
||||
if !self.contents.is_empty() {
|
||||
self.partial_action = Some(PartialAction::Replace);
|
||||
}
|
||||
}
|
||||
|
||||
const fn space(&mut self) {
|
||||
self.partial_action = Some(PartialAction::Space);
|
||||
}
|
||||
|
||||
const fn repeat(&mut self) {
|
||||
self.partial_action = Some(PartialAction::Repeat);
|
||||
}
|
||||
|
||||
const fn to(&mut self) {
|
||||
self.partial_action = Some(PartialAction::To);
|
||||
}
|
||||
|
||||
pub fn scroll_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= BYTES_OF_PADDING { return; }
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + BYTES_PER_LINE,
|
||||
self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE
|
||||
);
|
||||
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
pub fn scroll_up(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE);
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_cursor_half_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= BYTES_OF_PADDING { return; }
|
||||
|
||||
let old_scroll_position = self.scroll_position;
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE),
|
||||
self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE
|
||||
);
|
||||
|
||||
let scroll_position_change = self.scroll_position - old_scroll_position;
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.head = min(
|
||||
self.primary_cursor.head + scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
self.primary_cursor.tail = min(
|
||||
self.primary_cursor.tail + scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.head = (cursor.head + scroll_position_change).min(max_contents_index);
|
||||
cursor.tail = (cursor.tail + scroll_position_change).min(max_contents_index);
|
||||
}
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_cursor_half_up(&mut self, window_size: WindowSize) {
|
||||
let old_scroll_position = self.scroll_position;
|
||||
|
||||
self.scroll_position = self.scroll_position.saturating_sub(
|
||||
(window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE)
|
||||
);
|
||||
|
||||
let scroll_position_change = old_scroll_position - self.scroll_position;
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.head = min(
|
||||
self.primary_cursor.head - scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
self.primary_cursor.tail = min(
|
||||
self.primary_cursor.tail - scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.head = (cursor.head - scroll_position_change).min(max_contents_index);
|
||||
cursor.tail = (cursor.tail - scroll_position_change).min(max_contents_index);
|
||||
}
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= BYTES_OF_PADDING { return; }
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + window_size.visible_byte_count(),
|
||||
self.max_contents_index() - BYTES_OF_PADDING - self.max_contents_index() % BYTES_PER_LINE
|
||||
);
|
||||
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_up(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.scroll_position.saturating_sub(
|
||||
window_size.visible_byte_count()
|
||||
);
|
||||
|
||||
if window_size.hex_rows() > LINES_OF_PADDING * 2 {
|
||||
self.primary_cursor.clamp(
|
||||
self.scroll_position + BYTES_OF_PADDING,
|
||||
window_size.visible_byte_count() - BYTES_OF_PADDING * 2
|
||||
);
|
||||
} else {
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
}
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn collapse_selection(&mut self) {
|
||||
self.primary_cursor.collapse();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
fn flip_selection(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.flip();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.flip();
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn delete(&mut self, window_size: WindowSize) {
|
||||
if !self.contents.is_empty() {
|
||||
self.execute_and_add(
|
||||
EditAction::Delete {
|
||||
primary_cursor: self.primary_cursor,
|
||||
cursors: self.cursors.clone(),
|
||||
primary_old_data: self.contents[self.primary_cursor.range()].into(),
|
||||
old_data: self.cursors
|
||||
.iter()
|
||||
.map(|cursor| self.contents[cursor.range()].to_vec())
|
||||
.collect(),
|
||||
},
|
||||
window_size
|
||||
);
|
||||
}
|
||||
|
||||
if self.mode == Mode::Select {
|
||||
self.mode = Mode::Normal;
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(&mut self, window_size: WindowSize) {
|
||||
if self.time_traveling == Some(0) || self.edit_history.is_empty() { return; }
|
||||
|
||||
let current_date = self.time_traveling
|
||||
.map_or(self.edit_history.len() - 1, |date| date - 1);
|
||||
|
||||
self.time_traveling = Some(current_date);
|
||||
|
||||
let edit_action = replace(
|
||||
&mut self.edit_history[current_date],
|
||||
EditAction::Placeholder
|
||||
);
|
||||
|
||||
self.undo_edit(&edit_action, window_size);
|
||||
|
||||
self.edit_history[current_date] = edit_action;
|
||||
}
|
||||
|
||||
fn redo(&mut self, window_size: WindowSize) {
|
||||
let Some(previous_date) = self.time_traveling else { return; };
|
||||
|
||||
let current_date = previous_date + 1;
|
||||
|
||||
self.time_traveling = if current_date == self.edit_history.len() {
|
||||
None
|
||||
} else {
|
||||
Some(current_date)
|
||||
};
|
||||
|
||||
let edit_action = replace(
|
||||
&mut self.edit_history[previous_date],
|
||||
EditAction::Placeholder
|
||||
);
|
||||
|
||||
self.execute_edit(&edit_action, window_size);
|
||||
|
||||
self.edit_history[previous_date] = edit_action;
|
||||
}
|
||||
|
||||
fn save(&mut self) {
|
||||
let mut file = File::create(&self.file_path).unwrap();
|
||||
file.write_all(&self.contents).unwrap();
|
||||
|
||||
self.last_saved_at = Some(
|
||||
self.time_traveling.unwrap_or(self.edit_history.len())
|
||||
);
|
||||
}
|
||||
|
||||
fn copy_selection_on_next_line(&mut self, window_size: WindowSize) {
|
||||
let new_cursors: Vec<Cursor> = iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.filter_map(|cursor| {
|
||||
let number_of_lines_tall = (cursor.upper_bound() - cursor.lower_bound()) / BYTES_PER_LINE;
|
||||
let offset_to_add = (number_of_lines_tall + 1) * BYTES_PER_LINE;
|
||||
|
||||
if cursor.lower_bound() + offset_to_add < self.contents.len() {
|
||||
Some(
|
||||
Cursor {
|
||||
head: min(cursor.head + offset_to_add, self.max_contents_index()),
|
||||
tail: min(cursor.tail + offset_to_add, self.max_contents_index())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.cursors.extend(new_cursors);
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
|
||||
self.rotate_selections_forward(window_size);
|
||||
}
|
||||
|
||||
fn rotate_selections_backward(&mut self, window_size: WindowSize) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let next_cursor_index = self.cursors
|
||||
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
|
||||
if next_cursor_index == 0 {
|
||||
let cursor_count = self.cursors.len();
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[cursor_count - 1]);
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
} else {
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index - 1]);
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn rotate_selections_forward(&mut self, window_size: WindowSize) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let next_cursor_index = self.cursors
|
||||
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
if next_cursor_index == self.cursors.len() {
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[0]);
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
} else {
|
||||
swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index]);
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn keep_primary_selection(&mut self) {
|
||||
self.cursors.clear();
|
||||
}
|
||||
|
||||
fn remove_primary_selection(&mut self) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let next_cursor_index = self.cursors
|
||||
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
if next_cursor_index == self.cursors.len() {
|
||||
self.primary_cursor = self.cursors.remove(0);
|
||||
} else {
|
||||
self.primary_cursor = self.cursors.remove(next_cursor_index);
|
||||
}
|
||||
}
|
||||
|
||||
fn split_selections_into_size(&mut self, size: usize, window_size: WindowSize) {
|
||||
if !iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.all(|cursor| cursor.len().is_multiple_of(size))
|
||||
{
|
||||
self.alert_message = Span::from(
|
||||
format!("not all selections are a multiple of {size} long")
|
||||
).red();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut new_cursors = iter::once(self.primary_cursor)
|
||||
.chain(self.cursors.iter().copied())
|
||||
.flat_map(|cursor| {
|
||||
cursor
|
||||
.range()
|
||||
.step_by(size)
|
||||
.map(|tail| Cursor { head: tail + size - 1, tail })
|
||||
});
|
||||
|
||||
self.primary_cursor = new_cursors.next().unwrap();
|
||||
self.cursors = new_cursors.collect();
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn jump_to_selected_offset(&mut self, window_size: WindowSize) {
|
||||
if !iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.all(|cursor| {
|
||||
bytes_to_nat(&self.contents[cursor.range()])
|
||||
.map(|nat| nat as usize)
|
||||
.is_some_and(|offset| offset < self.contents.len())
|
||||
})
|
||||
{
|
||||
if self.cursors.is_empty() {
|
||||
self.alert_message = Span::from(
|
||||
"selection is not a valid offset"
|
||||
).red();
|
||||
} else {
|
||||
self.alert_message = Span::from(
|
||||
"not all selections are valid offsets"
|
||||
).red();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.primary_cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[self.primary_cursor.range()]).unwrap() as usize
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
*cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[cursor.range()]).unwrap() as usize
|
||||
);
|
||||
}
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn jump_to_selected_offset_relative_to_mark(&mut self, window_size: WindowSize) {
|
||||
let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect();
|
||||
sorted_marks.sort_unstable();
|
||||
|
||||
if !iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.all(|cursor| {
|
||||
bytes_to_nat(&self.contents[cursor.range()])
|
||||
.map(|offset| mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize)
|
||||
.is_some_and(|offset| offset < self.contents.len())
|
||||
})
|
||||
{
|
||||
if self.cursors.is_empty() {
|
||||
self.alert_message = Span::from(
|
||||
"selection is not a valid offset"
|
||||
).red();
|
||||
} else {
|
||||
self.alert_message = Span::from(
|
||||
"not all selections are valid offsets"
|
||||
).red();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.primary_cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[self.primary_cursor.range()])
|
||||
.map(|offset| {
|
||||
mark_before(self.primary_cursor.lower_bound(), &sorted_marks) + offset as usize
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
*cursor = Cursor::at(
|
||||
bytes_to_nat(&self.contents[cursor.range()])
|
||||
.map(|offset| {
|
||||
mark_before(cursor.lower_bound(), &sorted_marks) + offset as usize
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn toggle_mark(&mut self) {
|
||||
match self.marks.entry(self.primary_cursor.lower_bound()) {
|
||||
Entry::Occupied(occupied_entry) => { occupied_entry.remove(); },
|
||||
Entry::Vacant(vacant_entry) => vacant_entry.insert(),
|
||||
}
|
||||
|
||||
for cursor in &self.cursors {
|
||||
match self.marks.entry(cursor.lower_bound()) {
|
||||
Entry::Occupied(occupied_entry) => { occupied_entry.remove(); },
|
||||
Entry::Vacant(vacant_entry) => vacant_entry.insert(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn align_view_center(&mut self, window_size: WindowSize) {
|
||||
let half_a_screen = window_size.visible_byte_count() / 2;
|
||||
|
||||
self.scroll_position = self.primary_cursor.head
|
||||
.saturating_sub(self.primary_cursor.head % BYTES_PER_LINE)
|
||||
.saturating_sub(half_a_screen - (half_a_screen % BYTES_PER_LINE));
|
||||
}
|
||||
|
||||
fn align_view_bottom(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.primary_cursor.head
|
||||
.saturating_sub(self.primary_cursor.head % BYTES_PER_LINE)
|
||||
.saturating_sub(
|
||||
window_size
|
||||
.visible_byte_count()
|
||||
.saturating_sub(BYTES_PER_LINE + BYTES_OF_PADDING)
|
||||
)
|
||||
.min(self.max_contents_index() - self.max_contents_index() % BYTES_PER_LINE);
|
||||
}
|
||||
|
||||
const fn align_view_top(&mut self) {
|
||||
self.scroll_position = self.primary_cursor.head
|
||||
.saturating_sub(self.primary_cursor.head % BYTES_PER_LINE)
|
||||
.saturating_sub(BYTES_OF_PADDING);
|
||||
}
|
||||
|
||||
fn extend_to_mark(&mut self, window_size: WindowSize) {
|
||||
let mut sorted_marks: Vec<_> = self.marks.iter().copied().collect();
|
||||
sorted_marks.sort_unstable();
|
||||
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
let mark_after_primary = mark_after(
|
||||
self.primary_cursor.head,
|
||||
&sorted_marks,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
self.primary_cursor.tail = self.primary_cursor.head;
|
||||
self.primary_cursor.head = mark_after_primary - 1;
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
let mark_after_cursor = mark_after(
|
||||
cursor.head,
|
||||
&sorted_marks,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
cursor.tail = cursor.head;
|
||||
cursor.head = mark_after_cursor - 1;
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn extend_to_null(&mut self, window_size: WindowSize) {
|
||||
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0)
|
||||
{
|
||||
self.primary_cursor.tail = self.primary_cursor.head;
|
||||
self.primary_cursor.head += null_offset_after_primary;
|
||||
}
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
if let Some(null_offset_after_primary) = self.contents[cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0)
|
||||
{
|
||||
cursor.tail = cursor.head;
|
||||
cursor.head += null_offset_after_primary;
|
||||
}
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn extend_to_FF(&mut self, window_size: WindowSize) {
|
||||
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0xFF)
|
||||
{
|
||||
self.primary_cursor.tail = self.primary_cursor.head;
|
||||
self.primary_cursor.head += null_offset_after_primary;
|
||||
}
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
if let Some(null_offset_after_primary) = self.contents[cursor.head..]
|
||||
.iter()
|
||||
.skip(1)
|
||||
.position(|&byte| byte == 0xFF)
|
||||
{
|
||||
cursor.tail = cursor.head;
|
||||
cursor.head += null_offset_after_primary;
|
||||
}
|
||||
}
|
||||
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn inspect_selection(&mut self) {
|
||||
if self.inspection_status == Some(InspectionStatus::Normal) {
|
||||
self.inspection_status = None;
|
||||
return;
|
||||
}
|
||||
|
||||
self.inspection_status = Some(InspectionStatus::Normal);
|
||||
|
||||
self.popups.extend(
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.filter_map(|cursor| {
|
||||
let selection = &self.contents[cursor.range()];
|
||||
|
||||
let popup_lines = inspect(selection);
|
||||
|
||||
if popup_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Popup::new(cursor.lower_bound(), popup_lines))
|
||||
}
|
||||
})
|
||||
.sorted_unstable_by_key(|popup| popup.at)
|
||||
);
|
||||
|
||||
if self.popups.is_empty() {
|
||||
self.inspection_status = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_selection_color(&mut self) {
|
||||
if self.inspection_status == Some(InspectionStatus::ColorsOnly) {
|
||||
self.inspection_status = None;
|
||||
return;
|
||||
}
|
||||
|
||||
self.inspection_status = Some(InspectionStatus::ColorsOnly);
|
||||
|
||||
self.popups.extend(
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.filter_map(|cursor| {
|
||||
let selection = &self.contents[cursor.range()];
|
||||
|
||||
let popup_lines = inspect_color(selection);
|
||||
|
||||
if popup_lines.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Popup::new(cursor.lower_bound(), popup_lines))
|
||||
}
|
||||
})
|
||||
.sorted_unstable_by_key(|popup| popup.at)
|
||||
);
|
||||
|
||||
if self.popups.is_empty() {
|
||||
self.inspection_status = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect(selection: &[u8]) -> Vec<Span<'static>> {
|
||||
let nat = bytes_to_nat(selection);
|
||||
|
||||
let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len()));
|
||||
|
||||
let utf8 = str::from_utf8(selection).ok()
|
||||
.filter(|_| selection.len() == 1)
|
||||
.map(|utf8| utf8.trim_suffix('\0'))
|
||||
.filter(|utf8| !utf8.contains(is_illegal_control_character))
|
||||
.map(|utf8| Span::from(format!("\"{utf8}\"")).red());
|
||||
|
||||
let fixedpoint2012 = nat
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|nat| f64::from(nat as u32) / f64::from(1 << 12))
|
||||
.map(|fixedpoint2012| {
|
||||
let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint2012_signed = int
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|int| f64::from(int as i32) / f64::from(1 << 12))
|
||||
.map(|fixedpoint2012_signed| {
|
||||
let two_decimals_is_enough = (fixedpoint2012_signed * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("i20.12: {approximate_symbol}{fixedpoint2012_signed:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint1616 = nat
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|nat| f64::from(nat as u32) / f64::from(1 << 16))
|
||||
.map(|fixedpoint1616| {
|
||||
let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint1616_signed = int
|
||||
.filter(|_| selection.len() == 4)
|
||||
.map(|int| f64::from(int as i32) / f64::from(1 << 16))
|
||||
.map(|fixedpoint1616_signed| {
|
||||
let two_decimals_is_enough = (fixedpoint1616_signed * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("i16.16: {approximate_symbol}{fixedpoint1616_signed:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint124 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.map(|nat| f64::from(nat as u16) / f64::from(1 << 4))
|
||||
.map(|fixedpoint124| {
|
||||
let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint88 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.map(|nat| f64::from(nat as u16) / f64::from(1 << 8))
|
||||
.map(|fixedpoint88| {
|
||||
let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into()
|
||||
});
|
||||
|
||||
let fixedpoint412 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.map(|nat| f64::from(nat as u16) / f64::from(1 << 12))
|
||||
.map(|fixedpoint412| {
|
||||
let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0;
|
||||
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" };
|
||||
|
||||
format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into()
|
||||
});
|
||||
|
||||
let color888 = (selection.len() == 3)
|
||||
.then(|| [selection[0], selection[1], selection[2]])
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("#{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
let color555 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.filter(|&nat| nat >> 15 == 0)
|
||||
.map(|nat| color555_to_color888(nat as u16))
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("555: #{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
int.map(|int| format!("{int}").into())
|
||||
.into_iter()
|
||||
.chain(nat.map(|nat| format!("{nat}").into()))
|
||||
.chain(utf8)
|
||||
.chain(fixedpoint2012_signed)
|
||||
.chain(fixedpoint2012)
|
||||
.chain(fixedpoint1616_signed)
|
||||
.chain(fixedpoint1616)
|
||||
.chain(fixedpoint124)
|
||||
.chain(fixedpoint88)
|
||||
.chain(fixedpoint412)
|
||||
.chain(color888)
|
||||
.chain(color555)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn inspect_color(selection: &[u8]) -> Vec<Span<'static>> {
|
||||
let nat = bytes_to_nat(selection);
|
||||
|
||||
let color888 = (selection.len() == 3)
|
||||
.then(|| [selection[0], selection[1], selection[2]])
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("#{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
let color555 = nat
|
||||
.filter(|_| selection.len() == 2)
|
||||
.filter(|&nat| nat >> 15 == 0)
|
||||
.map(|nat| color555_to_color888(nat as u16))
|
||||
.map(|[red, green, blue]| {
|
||||
Span::from(format!("#{red:02X}{green:02X}{blue:02X}"))
|
||||
.fg(Color::Rgb(red, green, blue))
|
||||
|
||||
});
|
||||
|
||||
color888
|
||||
.into_iter()
|
||||
.chain(color555)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// MARK: helpers
|
||||
impl Buffer {
|
||||
pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) {
|
||||
if self.primary_cursor.head < self.scroll_position + BYTES_OF_PADDING {
|
||||
self.align_view_top();
|
||||
} else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(BYTES_OF_PADDING) {
|
||||
self.align_view_bottom(window_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_to_nat(bytes: &[u8]) -> Option<u64> {
|
||||
bytes
|
||||
.iter()
|
||||
.rev() // little-endian
|
||||
.skip_while(|&&byte| byte == 0)
|
||||
.try_fold(u64::default(), |result, &byte| {
|
||||
Some(result.shl_exact(8)? | u64::from(byte))
|
||||
})
|
||||
}
|
||||
|
||||
const fn nat_to_int_if_different(nat: u64, bytes: usize) -> Option<i64> {
|
||||
match bytes {
|
||||
1 if nat > i8::MAX as u64 => Some((nat as u8).cast_signed() as i64),
|
||||
2 if nat > i16::MAX as u64 => Some((nat as u16).cast_signed() as i64),
|
||||
4 if nat > i32::MAX as u64 => Some((nat as u32).cast_signed() as i64),
|
||||
8 if nat > i64::MAX as u64 => Some(nat.cast_signed()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_to_int_tests() {
|
||||
assert_eq!(nat_to_int_if_different(0, 1), None);
|
||||
assert_eq!(nat_to_int_if_different(i8::MAX as u64, 1), None);
|
||||
assert_eq!(nat_to_int_if_different(i8::MAX as u64 + 1, 1), Some(i8::MIN.into()));
|
||||
assert_eq!(nat_to_int_if_different(u8::MAX.into(), 1), Some(-1));
|
||||
|
||||
assert_eq!(nat_to_int_if_different(0, 2), None);
|
||||
assert_eq!(nat_to_int_if_different(i16::MAX as u64, 2), None);
|
||||
assert_eq!(nat_to_int_if_different(i16::MAX as u64 + 1, 2), Some(i16::MIN.into()));
|
||||
assert_eq!(nat_to_int_if_different(u16::MAX.into(), 2), Some(-1));
|
||||
}
|
||||
|
||||
// or 0 if no mark is before
|
||||
fn mark_before(offset: usize, sorted_marks: &[usize]) -> usize {
|
||||
match sorted_marks.binary_search(&offset) {
|
||||
Ok(_) => offset,
|
||||
Err(0) => 0,
|
||||
Err(mark_after_index) => sorted_marks[mark_after_index - 1],
|
||||
}
|
||||
}
|
||||
|
||||
// or end index if no mark is after
|
||||
fn mark_after(offset: usize, sorted_marks: &[usize], max: usize) -> usize {
|
||||
if sorted_marks.is_empty() { return max + 1; }
|
||||
|
||||
match sorted_marks.binary_search(&offset) {
|
||||
Ok(mark_before_index) => if mark_before_index == sorted_marks.len() - 1 {
|
||||
max + 1
|
||||
} else {
|
||||
sorted_marks[mark_before_index + 1]
|
||||
},
|
||||
Err(mark_after_index) => {
|
||||
if mark_after_index == sorted_marks.len() {
|
||||
max + 1
|
||||
} else {
|
||||
sorted_marks[mark_after_index]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_illegal_control_character(character: char) -> bool {
|
||||
match character {
|
||||
'\t' | '\n' | '\r' => false,
|
||||
_ if character.is_ascii_control() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
const fn color555_to_color888(color555: u16) -> [u8; 3] {
|
||||
[
|
||||
// 8 is the ratio between the number of colors in 555 vs 888 (32:256)
|
||||
(color555 & 0b11111) as u8 * 8,
|
||||
(color555 >> 5 & 0b11111) as u8 * 8,
|
||||
(color555 >> 10 & 0b11111) as u8 * 8
|
||||
]
|
||||
}
|
||||
+6
-399
@@ -2,6 +2,12 @@ use std::{cmp::min, iter};
|
||||
use ratatui::{layout::Rect, text::{Line, Text}, widgets::Widget};
|
||||
use crate::{BYTES_PER_LINE, buffer::Buffer};
|
||||
|
||||
mod address;
|
||||
mod hex;
|
||||
mod character_panel;
|
||||
mod status_line;
|
||||
mod extra_statuses;
|
||||
|
||||
impl Widget for &Buffer {
|
||||
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
|
||||
let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1);
|
||||
@@ -102,405 +108,6 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
mod address {
|
||||
use ratatui::{style::{Color, Style}, text::Span};
|
||||
|
||||
pub fn render_address(address: usize) -> Span<'static> {
|
||||
Span {
|
||||
style: style_for_address(address),
|
||||
content: format!("{address:08x}").into()
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn style_for_address(address: usize) -> Style {
|
||||
if address.is_multiple_of(0x100) {
|
||||
Style::new().fg(Color::Rgb(0x68, 0x99, 0xA0))
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(0x8A, 0xBB, 0xC3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod hex {
|
||||
use std::{borrow::Cow, iter::{self, repeat_n}, mem};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{style::{Color, Style, Stylize}, text::Span};
|
||||
|
||||
use crate::{BYTES_PER_CHUNK, BYTES_PER_LINE, CHUNKS_PER_LINE, buffer::{Buffer, Mode, PartialAction}, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span};
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_chunks(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8; BYTES_PER_LINE]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
|
||||
|
||||
assert!(remainder.is_empty(), "BYTES_PER_LINE should be a multiple of BYTES_PER_CHUNK");
|
||||
|
||||
#[allow(unstable_name_collisions)]
|
||||
chunks
|
||||
.iter()
|
||||
.copied()
|
||||
.zip((address..).step_by(BYTES_PER_CHUNK))
|
||||
.flat_map(|(chunk, address)| {
|
||||
self.render_chunk(address, &chunk).collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_partial_chunks(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
|
||||
|
||||
let remainder_address = address + chunks.len() * BYTES_PER_CHUNK;
|
||||
#[allow(clippy::if_not_else)]
|
||||
let remainder_chunks: Option<Vec<_>> = if !remainder.is_empty() {
|
||||
Some(self.render_partial_chunk(remainder_address, remainder).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chunks_rendered = chunks.len() + remainder_chunks.iter().len();
|
||||
let chunks_not_rendered = CHUNKS_PER_LINE - chunks_rendered;
|
||||
let spaces_per_chunk = BYTES_PER_CHUNK - 1 + 2;
|
||||
let bytes_not_rendered = BYTES_PER_LINE - bytes.len();
|
||||
|
||||
let padding_width = 2 * bytes_not_rendered +
|
||||
spaces_per_chunk * chunks_not_rendered;
|
||||
|
||||
#[allow(unstable_name_collisions)]
|
||||
chunks
|
||||
.iter()
|
||||
.copied()
|
||||
.zip((address..).step_by(BYTES_PER_CHUNK))
|
||||
.map(|(chunk, address)| self.render_chunk(address, &chunk).collect())
|
||||
.chain(remainder_chunks)
|
||||
.flatten()
|
||||
.chain(repeat_n(" ".into(), padding_width))
|
||||
}
|
||||
|
||||
fn render_chunk(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8; BYTES_PER_CHUNK]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
iter::once(self.render_large_space_before(address))
|
||||
.chain(
|
||||
bytes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(address..)
|
||||
.map(|(byte, address)| self.render_byte_at(address, byte))
|
||||
.interleave(
|
||||
(address..)
|
||||
.take(BYTES_PER_CHUNK)
|
||||
.skip(1)
|
||||
.map(|address| self.render_space_before(address))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn render_partial_chunk(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
iter::once(self.render_large_space_before(address))
|
||||
.chain(
|
||||
bytes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(address..)
|
||||
.map(|(byte, address)| self.render_byte_at(address, byte))
|
||||
.interleave(
|
||||
(address..)
|
||||
.take(BYTES_PER_CHUNK)
|
||||
.skip(1)
|
||||
.map(|address| self.render_space_before(address))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn render_byte_at(
|
||||
&self,
|
||||
address: usize,
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
if self.partial_action == Some(PartialAction::Replace) &&
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains(address).is_some())
|
||||
{
|
||||
let replaced_byte = self.partial_replace.unwrap_or(0) << 4;
|
||||
|
||||
self.render_byte_without_replace_preview(address, replaced_byte)
|
||||
.black()
|
||||
} else {
|
||||
self.render_byte_without_replace_preview(address, byte)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_byte_without_replace_preview(
|
||||
&self,
|
||||
address: usize,
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table();
|
||||
|
||||
let span = SPAN_FOR_BYTE[byte as usize].clone();
|
||||
|
||||
if let Some(place_in_cursor) = self.primary_cursor.contains(address) {
|
||||
let head_color = match self.mode {
|
||||
Mode::Select => Color::Yellow,
|
||||
_ => Color::Gray
|
||||
};
|
||||
|
||||
match place_in_cursor {
|
||||
InCursor::Head => span.bg(head_color),
|
||||
InCursor::Rest => span.bg(Color::selection_tail_grey()),
|
||||
}
|
||||
} else {
|
||||
match self.cursors
|
||||
.iter()
|
||||
.find_map(|cursor| cursor.contains(address))
|
||||
{
|
||||
Some(InCursor::Head) => span.bg(Color::secondary_selection_head_grey()),
|
||||
Some(InCursor::Rest) => span.bg(Color::selection_tail_grey()),
|
||||
None => span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_large_space_before(&self, address: usize) -> Span<'static> {
|
||||
let span: Span = if self.marks.contains(&address) {
|
||||
" →".into()
|
||||
} else {
|
||||
" ".into()
|
||||
};
|
||||
|
||||
if !address.is_multiple_of(BYTES_PER_LINE) &&
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains_space_before(address))
|
||||
{
|
||||
span.bg(Color::selection_tail_grey())
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
|
||||
fn render_space_before(&self, address: usize) -> Span<'static> {
|
||||
let span: Span = if self.marks.contains(&address) {
|
||||
"→".into()
|
||||
} else {
|
||||
" ".into()
|
||||
};
|
||||
|
||||
if iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains_space_before(address))
|
||||
{
|
||||
span.bg(Color::selection_tail_grey())
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn create_byte_lookup_table() -> [Span<'static>; u8::CARDINALITY] {
|
||||
let mut result = [const { empty_span() }; u8::CARDINALITY];
|
||||
|
||||
let mut index = 0;
|
||||
while index < u8::CARDINALITY {
|
||||
result[index].style = style_for_byte(index as u8);
|
||||
mem::forget(mem::replace(&mut result[index].content, content_for_byte(index as u8)));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
const fn style_for_byte(byte: u8) -> Style {
|
||||
Style::new().fg(fg_for_byte(byte))
|
||||
}
|
||||
|
||||
const fn fg_for_byte(byte: u8) -> Color {
|
||||
match byte {
|
||||
0x00 => Color::Rgb(0x80, 0x80, 0x80), // grey
|
||||
0x01..0x10 => Color::Rgb(0xFF, 0x71, 0xA9), // red
|
||||
0x10..0x20 => Color::Rgb(0xFF, 0x7A, 0x78), // salmon
|
||||
0x20..0x30 => Color::Rgb(0xFF, 0x81, 0x23), // red-orange
|
||||
0x30..0x40 => Color::Rgb(0xF7, 0x93, 0x00), // yellow-orange
|
||||
0x40..0x50 => Color::Rgb(0xE6, 0x9F, 0x00), // yellow
|
||||
0x50..0x60 => Color::Rgb(0xC1, 0xB2, 0x00), // green-yellow
|
||||
0x60..0x70 => Color::Rgb(0x82, 0xC6, 0x00), // lime
|
||||
0x70..0x80 => Color::Rgb(0x00, 0xD5, 0x00), // green
|
||||
0x80..0x90 => Color::Rgb(0x00, 0xD4, 0x59), // clover
|
||||
0x90..0xA0 => Color::Rgb(0x00, 0xD0, 0x91), // teal
|
||||
0xA0..0xB0 => Color::Rgb(0x00, 0xCC, 0xBB), // cyan
|
||||
0xB0..0xC0 => Color::Rgb(0x00, 0xC7, 0xDE), // light blue
|
||||
0xC0..0xD0 => Color::Rgb(0x00, 0xBE, 0xFF), // blue
|
||||
0xD0..0xE0 => Color::Rgb(0x6C, 0xAF, 0xFF), // blurple
|
||||
0xE0..0xF0 => Color::Rgb(0xB2, 0x98, 0xFF), // purple
|
||||
0xF0..0xFF => Color::Rgb(0xFF, 0x4D, 0xFF), // pink
|
||||
0xFF => Color::White
|
||||
}
|
||||
}
|
||||
|
||||
const fn content_for_byte(byte: u8) -> Cow<'static, str> {
|
||||
Cow::Borrowed(hex_for_byte(byte))
|
||||
}
|
||||
|
||||
const fn hex_for_byte(byte: u8) -> &'static str {
|
||||
const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff"];
|
||||
|
||||
LOOK_UP_TABLE[byte as usize]
|
||||
}
|
||||
}
|
||||
|
||||
mod character_panel {
|
||||
use std::{borrow::Cow, iter, mem};
|
||||
use ratatui::{style::{Color, Style, Stylize}, text::Span};
|
||||
use crate::{buffer::Buffer, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span};
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_character_panel(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
bytes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(address..)
|
||||
.map(|(byte, address)| self.render_character_at(address, byte))
|
||||
}
|
||||
|
||||
fn render_character_at(
|
||||
&self,
|
||||
address: usize,
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_character_lookup_table();
|
||||
|
||||
let span = SPAN_FOR_BYTE[byte as usize].clone();
|
||||
|
||||
match iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.find_map(|cursor| cursor.contains(address))
|
||||
{
|
||||
Some(InCursor::Head) => span.bg(Color::selection_tail_grey()),
|
||||
Some(InCursor::Rest) => span.on_dark_gray(),
|
||||
None => span,
|
||||
}
|
||||
}
|
||||
}
|
||||
const fn create_character_lookup_table() -> [Span<'static>; u8::CARDINALITY] {
|
||||
let mut result = [const { empty_span() }; u8::CARDINALITY];
|
||||
|
||||
let mut index = 0;
|
||||
while index < u8::CARDINALITY {
|
||||
result[index].style = style_for_character(index as u8);
|
||||
mem::forget(mem::replace(
|
||||
&mut result[index].content,
|
||||
content_for_character(index as u8)
|
||||
));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
const fn style_for_character(byte: u8) -> Style {
|
||||
Style::new().fg(fg_for_character(byte))
|
||||
}
|
||||
|
||||
const fn fg_for_character(byte: u8) -> Color {
|
||||
match byte {
|
||||
b'\0' => Color::Rgb(0xA0, 0xA0, 0xA0), // grey
|
||||
b'\t' | b'\n' | b'\r' | b' ' => Color::Red,
|
||||
_ if byte.is_ascii_graphic() => Color::Red,
|
||||
_ if byte.is_ascii() => Color::Green,
|
||||
0xFF => Color::White,
|
||||
_ => Color::Yellow,
|
||||
}
|
||||
}
|
||||
|
||||
const fn content_for_character(byte: u8) -> Cow<'static, str> {
|
||||
Cow::Borrowed(character_for_byte(byte))
|
||||
}
|
||||
|
||||
const fn character_for_byte(byte: u8) -> &'static str {
|
||||
const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["⋄", "•", "•", "•", "•", "•", "•", "•", "•", "→", "⏎", "•", "•", "␍", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "•", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "╳"];
|
||||
|
||||
LOOK_UP_TABLE[byte as usize]
|
||||
}
|
||||
}
|
||||
|
||||
mod status_line {
|
||||
use crate::{buffer::Buffer, custom_greys::CustomGreys};
|
||||
use ratatui::{style::{Color, Stylize}, text::{Line, Span, Text}};
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_status_line(&self) -> Text<'_> {
|
||||
Text::from(
|
||||
Line::from_iter([
|
||||
self.render_mode(),
|
||||
" ".into(),
|
||||
self.render_file_name(),
|
||||
self.modified_indicator(),
|
||||
" ".into(),
|
||||
self.alert_message.clone()
|
||||
])
|
||||
)
|
||||
.bg(Color::ui_grey())
|
||||
}
|
||||
|
||||
fn render_mode(&self) -> Span<'static> {
|
||||
Span::from(self.mode.label())
|
||||
.black()
|
||||
.bg(self.mode.color())
|
||||
}
|
||||
|
||||
fn render_file_name(&self) -> Span<'_> {
|
||||
Span::from(&self.file_name)
|
||||
}
|
||||
|
||||
fn modified_indicator(&self) -> Span<'static> {
|
||||
if self.has_unsaved_changes() {
|
||||
" [+]".into()
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod extra_statuses {
|
||||
use crate::buffer::Buffer;
|
||||
use ratatui::text::Line;
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_extra_statuses(&self) -> Line<'_> {
|
||||
let partial_action = self.partial_action
|
||||
.as_ref()
|
||||
.map_or("", |partial_action| partial_action.label());
|
||||
|
||||
if self.contents.is_empty() {
|
||||
format!("{partial_action} ").into()
|
||||
} else {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let percentage = self.primary_cursor.head as f64 / self.max_contents_index() as f64 * 100.0;
|
||||
|
||||
format!("{partial_action} {percentage:.0}% ").into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn byte_column_to_screen_column(byte_column: usize) -> usize {
|
||||
match byte_column {
|
||||
0 => 10,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use ratatui::{style::{Color, Style}, text::Span};
|
||||
|
||||
pub fn render_address(address: usize) -> Span<'static> {
|
||||
Span {
|
||||
style: style_for_address(address),
|
||||
content: format!("{address:08x}").into()
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn style_for_address(address: usize) -> Style {
|
||||
if address.is_multiple_of(0x100) {
|
||||
Style::new().fg(Color::Rgb(0x68, 0x99, 0xA0))
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(0x8A, 0xBB, 0xC3))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::{borrow::Cow, iter, mem};
|
||||
use ratatui::{style::{Color, Style, Stylize}, text::Span};
|
||||
use crate::{buffer::Buffer, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span};
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_character_panel(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
bytes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(address..)
|
||||
.map(|(byte, address)| self.render_character_at(address, byte))
|
||||
}
|
||||
|
||||
fn render_character_at(
|
||||
&self,
|
||||
address: usize,
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_character_lookup_table();
|
||||
|
||||
let span = SPAN_FOR_BYTE[byte as usize].clone();
|
||||
|
||||
match iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.find_map(|cursor| cursor.contains(address))
|
||||
{
|
||||
Some(InCursor::Head) => span.bg(Color::selection_tail_grey()),
|
||||
Some(InCursor::Rest) => span.on_dark_gray(),
|
||||
None => span,
|
||||
}
|
||||
}
|
||||
}
|
||||
const fn create_character_lookup_table() -> [Span<'static>; u8::CARDINALITY] {
|
||||
let mut result = [const { empty_span() }; u8::CARDINALITY];
|
||||
|
||||
let mut index = 0;
|
||||
while index < u8::CARDINALITY {
|
||||
result[index].style = style_for_character(index as u8);
|
||||
mem::forget(mem::replace(
|
||||
&mut result[index].content,
|
||||
content_for_character(index as u8)
|
||||
));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
const fn style_for_character(byte: u8) -> Style {
|
||||
Style::new().fg(fg_for_character(byte))
|
||||
}
|
||||
|
||||
const fn fg_for_character(byte: u8) -> Color {
|
||||
match byte {
|
||||
b'\0' => Color::Rgb(0xA0, 0xA0, 0xA0), // grey
|
||||
b'\t' | b'\n' | b'\r' | b' ' => Color::Red,
|
||||
_ if byte.is_ascii_graphic() => Color::Red,
|
||||
_ if byte.is_ascii() => Color::Green,
|
||||
0xFF => Color::White,
|
||||
_ => Color::Yellow,
|
||||
}
|
||||
}
|
||||
|
||||
const fn content_for_character(byte: u8) -> Cow<'static, str> {
|
||||
Cow::Borrowed(character_for_byte(byte))
|
||||
}
|
||||
|
||||
const fn character_for_byte(byte: u8) -> &'static str {
|
||||
const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["⋄", "•", "•", "•", "•", "•", "•", "•", "•", "→", "⏎", "•", "•", "␍", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", "•", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "•", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "╳"];
|
||||
|
||||
LOOK_UP_TABLE[byte as usize]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::buffer::{Buffer, PartialAction};
|
||||
use ratatui::text::Line;
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_extra_statuses(&self) -> Line<'_> {
|
||||
let partial_action = self.partial_action
|
||||
.as_ref()
|
||||
.map_or("", |partial_action| partial_action.label());
|
||||
|
||||
if self.contents.is_empty() {
|
||||
format!("{partial_action} ").into()
|
||||
} else {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let percentage = self.primary_cursor.head as f64 / self.max_contents_index() as f64 * 100.0;
|
||||
|
||||
format!("{partial_action} {percentage:.0}% ").into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialAction {
|
||||
pub const fn label(self) -> &'static str {
|
||||
use PartialAction::*;
|
||||
|
||||
match self {
|
||||
Goto => "g",
|
||||
View => "z",
|
||||
Replace => "r",
|
||||
Space => "␠",
|
||||
Repeat => "×",
|
||||
To => "t",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
use std::{borrow::Cow, iter::{self, repeat_n}, mem};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{style::{Color, Style, Stylize}, text::Span};
|
||||
|
||||
use crate::{BYTES_PER_CHUNK, BYTES_PER_LINE, CHUNKS_PER_LINE, buffer::{Buffer, Mode, PartialAction}, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span};
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_chunks(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8; BYTES_PER_LINE]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
|
||||
|
||||
assert!(remainder.is_empty(), "BYTES_PER_LINE should be a multiple of BYTES_PER_CHUNK");
|
||||
|
||||
#[allow(unstable_name_collisions)]
|
||||
chunks
|
||||
.iter()
|
||||
.copied()
|
||||
.zip((address..).step_by(BYTES_PER_CHUNK))
|
||||
.flat_map(|(chunk, address)| {
|
||||
self.render_chunk(address, &chunk).collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_partial_chunks(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
|
||||
|
||||
let remainder_address = address + chunks.len() * BYTES_PER_CHUNK;
|
||||
#[allow(clippy::if_not_else)]
|
||||
let remainder_chunks: Option<Vec<_>> = if !remainder.is_empty() {
|
||||
Some(self.render_partial_chunk(remainder_address, remainder).collect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chunks_rendered = chunks.len() + remainder_chunks.iter().len();
|
||||
let chunks_not_rendered = CHUNKS_PER_LINE - chunks_rendered;
|
||||
let spaces_per_chunk = BYTES_PER_CHUNK - 1 + 2;
|
||||
let bytes_not_rendered = BYTES_PER_LINE - bytes.len();
|
||||
|
||||
let padding_width = 2 * bytes_not_rendered +
|
||||
spaces_per_chunk * chunks_not_rendered;
|
||||
|
||||
#[allow(unstable_name_collisions)]
|
||||
chunks
|
||||
.iter()
|
||||
.copied()
|
||||
.zip((address..).step_by(BYTES_PER_CHUNK))
|
||||
.map(|(chunk, address)| self.render_chunk(address, &chunk).collect())
|
||||
.chain(remainder_chunks)
|
||||
.flatten()
|
||||
.chain(repeat_n(" ".into(), padding_width))
|
||||
}
|
||||
|
||||
fn render_chunk(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8; BYTES_PER_CHUNK]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
iter::once(self.render_large_space_before(address))
|
||||
.chain(
|
||||
bytes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(address..)
|
||||
.map(|(byte, address)| self.render_byte_at(address, byte))
|
||||
.interleave(
|
||||
(address..)
|
||||
.take(BYTES_PER_CHUNK)
|
||||
.skip(1)
|
||||
.map(|address| self.render_space_before(address))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn render_partial_chunk(
|
||||
&self,
|
||||
address: usize,
|
||||
bytes: &[u8]
|
||||
) -> impl Iterator<Item=Span<'static>> {
|
||||
iter::once(self.render_large_space_before(address))
|
||||
.chain(
|
||||
bytes
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(address..)
|
||||
.map(|(byte, address)| self.render_byte_at(address, byte))
|
||||
.interleave(
|
||||
(address..)
|
||||
.take(BYTES_PER_CHUNK)
|
||||
.skip(1)
|
||||
.map(|address| self.render_space_before(address))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn render_byte_at(
|
||||
&self,
|
||||
address: usize,
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
if self.partial_action == Some(PartialAction::Replace) &&
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains(address).is_some())
|
||||
{
|
||||
let replaced_byte = self.partial_replace.unwrap_or(0) << 4;
|
||||
|
||||
self.render_byte_without_replace_preview(address, replaced_byte)
|
||||
.black()
|
||||
} else {
|
||||
self.render_byte_without_replace_preview(address, byte)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_byte_without_replace_preview(
|
||||
&self,
|
||||
address: usize,
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table();
|
||||
|
||||
let span = SPAN_FOR_BYTE[byte as usize].clone();
|
||||
|
||||
if let Some(place_in_cursor) = self.primary_cursor.contains(address) {
|
||||
let head_color = match self.mode {
|
||||
Mode::Select => Color::Yellow,
|
||||
_ => Color::Gray
|
||||
};
|
||||
|
||||
match place_in_cursor {
|
||||
InCursor::Head => span.bg(head_color),
|
||||
InCursor::Rest => span.bg(Color::selection_tail_grey()),
|
||||
}
|
||||
} else {
|
||||
match self.cursors
|
||||
.iter()
|
||||
.find_map(|cursor| cursor.contains(address))
|
||||
{
|
||||
Some(InCursor::Head) => span.bg(Color::secondary_selection_head_grey()),
|
||||
Some(InCursor::Rest) => span.bg(Color::selection_tail_grey()),
|
||||
None => span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_large_space_before(&self, address: usize) -> Span<'static> {
|
||||
let span: Span = if self.marks.contains(&address) {
|
||||
" →".into()
|
||||
} else {
|
||||
" ".into()
|
||||
};
|
||||
|
||||
if !address.is_multiple_of(BYTES_PER_LINE) &&
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains_space_before(address))
|
||||
{
|
||||
span.bg(Color::selection_tail_grey())
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
|
||||
fn render_space_before(&self, address: usize) -> Span<'static> {
|
||||
let span: Span = if self.marks.contains(&address) {
|
||||
"→".into()
|
||||
} else {
|
||||
" ".into()
|
||||
};
|
||||
|
||||
if iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains_space_before(address))
|
||||
{
|
||||
span.bg(Color::selection_tail_grey())
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn create_byte_lookup_table() -> [Span<'static>; u8::CARDINALITY] {
|
||||
let mut result = [const { empty_span() }; u8::CARDINALITY];
|
||||
|
||||
let mut index = 0;
|
||||
while index < u8::CARDINALITY {
|
||||
result[index].style = style_for_byte(index as u8);
|
||||
mem::forget(mem::replace(&mut result[index].content, content_for_byte(index as u8)));
|
||||
index += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
const fn style_for_byte(byte: u8) -> Style {
|
||||
Style::new().fg(fg_for_byte(byte))
|
||||
}
|
||||
|
||||
const fn fg_for_byte(byte: u8) -> Color {
|
||||
match byte {
|
||||
0x00 => Color::Rgb(0x80, 0x80, 0x80), // grey
|
||||
0x01..0x10 => Color::Rgb(0xFF, 0x71, 0xA9), // red
|
||||
0x10..0x20 => Color::Rgb(0xFF, 0x7A, 0x78), // salmon
|
||||
0x20..0x30 => Color::Rgb(0xFF, 0x81, 0x23), // red-orange
|
||||
0x30..0x40 => Color::Rgb(0xF7, 0x93, 0x00), // yellow-orange
|
||||
0x40..0x50 => Color::Rgb(0xE6, 0x9F, 0x00), // yellow
|
||||
0x50..0x60 => Color::Rgb(0xC1, 0xB2, 0x00), // green-yellow
|
||||
0x60..0x70 => Color::Rgb(0x82, 0xC6, 0x00), // lime
|
||||
0x70..0x80 => Color::Rgb(0x00, 0xD5, 0x00), // green
|
||||
0x80..0x90 => Color::Rgb(0x00, 0xD4, 0x59), // clover
|
||||
0x90..0xA0 => Color::Rgb(0x00, 0xD0, 0x91), // teal
|
||||
0xA0..0xB0 => Color::Rgb(0x00, 0xCC, 0xBB), // cyan
|
||||
0xB0..0xC0 => Color::Rgb(0x00, 0xC7, 0xDE), // light blue
|
||||
0xC0..0xD0 => Color::Rgb(0x00, 0xBE, 0xFF), // blue
|
||||
0xD0..0xE0 => Color::Rgb(0x6C, 0xAF, 0xFF), // blurple
|
||||
0xE0..0xF0 => Color::Rgb(0xB2, 0x98, 0xFF), // purple
|
||||
0xF0..0xFF => Color::Rgb(0xFF, 0x4D, 0xFF), // pink
|
||||
0xFF => Color::White
|
||||
}
|
||||
}
|
||||
|
||||
const fn content_for_byte(byte: u8) -> Cow<'static, str> {
|
||||
Cow::Borrowed(hex_for_byte(byte))
|
||||
}
|
||||
|
||||
const fn hex_for_byte(byte: u8) -> &'static str {
|
||||
const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff"];
|
||||
|
||||
LOOK_UP_TABLE[byte as usize]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::{buffer::{Buffer, Mode}, custom_greys::CustomGreys};
|
||||
use ratatui::{style::{Color, Stylize}, text::{Line, Span, Text}};
|
||||
|
||||
impl Buffer {
|
||||
pub fn render_status_line(&self) -> Text<'_> {
|
||||
Text::from(
|
||||
Line::from_iter([
|
||||
self.render_mode(),
|
||||
" ".into(),
|
||||
self.render_file_name(),
|
||||
self.modified_indicator(),
|
||||
" ".into(),
|
||||
self.alert_message.clone()
|
||||
])
|
||||
)
|
||||
.bg(Color::ui_grey())
|
||||
}
|
||||
|
||||
fn render_mode(&self) -> Span<'static> {
|
||||
Span::from(self.mode.label())
|
||||
.black()
|
||||
.bg(self.mode.color())
|
||||
}
|
||||
|
||||
fn render_file_name(&self) -> Span<'_> {
|
||||
Span::from(&self.file_name)
|
||||
}
|
||||
|
||||
fn modified_indicator(&self) -> Span<'static> {
|
||||
if self.has_unsaved_changes() {
|
||||
" [+]".into()
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => " NORMAL ",
|
||||
Self::Select => " SELECT ",
|
||||
Self::Insert => " INSERT ",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn color(self) -> Color {
|
||||
match self {
|
||||
Self::Normal => Color::Blue,
|
||||
Self::Select => Color::Yellow,
|
||||
Self::Insert => Color::Green,
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-268
@@ -1,8 +1,10 @@
|
||||
use std::{collections::{HashMap, hash_map::Entry}, env::{self, home_dir}, fmt::{self, Formatter}, fs::read_to_string, io, path::PathBuf};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use crate::{action::{Action, AppAction, BufferAction, CursorAction}, buffer::{Mode, PartialAction}};
|
||||
use crate::{action::Action, buffer::{Mode, PartialAction}};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::{Error, MapAccess, Unexpected, Visitor}, ser::SerializeMap};
|
||||
|
||||
mod default;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Config(
|
||||
@@ -46,8 +48,11 @@ impl Config {
|
||||
home_dir().map(|home| home.join("AppData").join("Roaming"))
|
||||
}
|
||||
|
||||
pub fn init() -> Result<Self, ConfigInitError> {
|
||||
let path = Self::path().ok_or(ConfigInitError::NoConfigPath)?;
|
||||
pub fn init(override_path: Option<PathBuf>) -> Result<Self, ConfigInitError> {
|
||||
let path = override_path
|
||||
.or_else(Self::path)
|
||||
.ok_or(ConfigInitError::NoConfigPath)?;
|
||||
|
||||
let raw_config = read_to_string(path)?;
|
||||
|
||||
Ok(toml::from_str(&raw_config)?)
|
||||
@@ -291,268 +296,3 @@ impl From<KeyEvent> for Keypress {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn default() -> Self {
|
||||
use AppAction::*;
|
||||
use BufferAction::*;
|
||||
use CursorAction::*;
|
||||
|
||||
[
|
||||
(Mode::Normal, [
|
||||
(None, [
|
||||
("q".try_into().unwrap(), QuitIfSaved.into()),
|
||||
("Q".try_into().unwrap(), Quit.into()),
|
||||
|
||||
("v".try_into().unwrap(), SelectMode.into()),
|
||||
|
||||
("g".try_into().unwrap(), Goto.into()),
|
||||
("z".try_into().unwrap(), View.into()),
|
||||
("r".try_into().unwrap(), Replace.into()),
|
||||
(" ".try_into().unwrap(), Space.into()),
|
||||
("*".try_into().unwrap(), Repeat.into()),
|
||||
("t".try_into().unwrap(), To.into()),
|
||||
|
||||
("i".try_into().unwrap(), MoveByteUp.into()),
|
||||
("k".try_into().unwrap(), MoveByteDown.into()),
|
||||
("j".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("l".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), MoveByteUp.into()),
|
||||
("down".try_into().unwrap(), MoveByteDown.into()),
|
||||
("left".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("right".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("G".try_into().unwrap(), GotoFileEnd.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), MoveNextWordStart.into()),
|
||||
("e".try_into().unwrap(), MoveNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), MovePreviousWordStart.into()),
|
||||
|
||||
(";".try_into().unwrap(), CollapseSelection.into()),
|
||||
("A-;".try_into().unwrap(), FlipSelections.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("u".try_into().unwrap(), Undo.into()),
|
||||
("U".try_into().unwrap(), Redo.into()),
|
||||
|
||||
("C-j".try_into().unwrap(), PreviousBuffer.into()),
|
||||
("C-l".try_into().unwrap(), NextBuffer.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
|
||||
("(".try_into().unwrap(), RotateSelectionsBackward.into()),
|
||||
(")".try_into().unwrap(), RotateSelectionsForward.into()),
|
||||
|
||||
(",".try_into().unwrap(), KeepPrimarySelection.into()),
|
||||
("A-,".try_into().unwrap(), RemovePrimarySelection.into()),
|
||||
|
||||
("1".try_into().unwrap(), SplitSelectionsInto1s.into()),
|
||||
("2".try_into().unwrap(), SplitSelectionsInto2s.into()),
|
||||
("3".try_into().unwrap(), SplitSelectionsInto3s.into()),
|
||||
("4".try_into().unwrap(), SplitSelectionsInto4s.into()),
|
||||
("5".try_into().unwrap(), SplitSelectionsInto5s.into()),
|
||||
("6".try_into().unwrap(), SplitSelectionsInto6s.into()),
|
||||
("7".try_into().unwrap(), SplitSelectionsInto7s.into()),
|
||||
("8".try_into().unwrap(), SplitSelectionsInto8s.into()),
|
||||
("9".try_into().unwrap(), SplitSelectionsInto9s.into()),
|
||||
|
||||
("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()),
|
||||
("A-J".try_into().unwrap(), JumpToSelectedOffset.into()),
|
||||
|
||||
("m".try_into().unwrap(), ToggleMark.into()),
|
||||
|
||||
("y".try_into().unwrap(), Yank.into()),
|
||||
|
||||
("C- ".try_into().unwrap(), InspectSelection.into()),
|
||||
("A- ".try_into().unwrap(), InspectSelectionColor.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Goto), [
|
||||
("j".try_into().unwrap(), GotoLineStart.into()),
|
||||
("l".try_into().unwrap(), GotoLineEnd.into()),
|
||||
|
||||
("g".try_into().unwrap(), GotoFileStart.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::View), [
|
||||
("z".try_into().unwrap(), AlignViewCenter.into()),
|
||||
("b".try_into().unwrap(), AlignViewBottom.into()),
|
||||
("t".try_into().unwrap(), AlignViewTop.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Space), [
|
||||
("w".try_into().unwrap(), Save.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Repeat), [
|
||||
("i".try_into().unwrap(), MoveByteUp.into()),
|
||||
("k".try_into().unwrap(), MoveByteDown.into()),
|
||||
("j".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("l".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), MoveByteUp.into()),
|
||||
("down".try_into().unwrap(), MoveByteDown.into()),
|
||||
("left".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("right".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), MoveNextWordStart.into()),
|
||||
("e".try_into().unwrap(), MoveNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), MovePreviousWordStart.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::To), [
|
||||
("m".try_into().unwrap(), ExtendToMark.into()),
|
||||
("0".try_into().unwrap(), ExtendToNull.into()),
|
||||
("f".try_into().unwrap(), ExtendToFF.into()),
|
||||
].into()),
|
||||
].into()),
|
||||
(Mode::Select, [
|
||||
(None, [
|
||||
("q".try_into().unwrap(), QuitIfSaved.into()),
|
||||
("Q".try_into().unwrap(), Quit.into()),
|
||||
|
||||
("v".try_into().unwrap(), NormalMode.into()),
|
||||
|
||||
("g".try_into().unwrap(), Goto.into()),
|
||||
("z".try_into().unwrap(), View.into()),
|
||||
("r".try_into().unwrap(), Replace.into()),
|
||||
(" ".try_into().unwrap(), Space.into()),
|
||||
("*".try_into().unwrap(), Repeat.into()),
|
||||
("t".try_into().unwrap(), To.into()),
|
||||
|
||||
("i".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("k".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("j".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("l".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("down".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("left".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("right".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), ExtendNextWordStart.into()),
|
||||
("e".try_into().unwrap(), ExtendNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), ExtendPreviousWordStart.into()),
|
||||
|
||||
(";".try_into().unwrap(), CollapseSelection.into()),
|
||||
("A-;".try_into().unwrap(), FlipSelections.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("u".try_into().unwrap(), Undo.into()),
|
||||
("U".try_into().unwrap(), Redo.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
|
||||
("(".try_into().unwrap(), RotateSelectionsBackward.into()),
|
||||
(")".try_into().unwrap(), RotateSelectionsForward.into()),
|
||||
|
||||
(",".try_into().unwrap(), KeepPrimarySelection.into()),
|
||||
("A-,".try_into().unwrap(), RemovePrimarySelection.into()),
|
||||
|
||||
("1".try_into().unwrap(), SplitSelectionsInto1s.into()),
|
||||
("2".try_into().unwrap(), SplitSelectionsInto2s.into()),
|
||||
("3".try_into().unwrap(), SplitSelectionsInto3s.into()),
|
||||
("4".try_into().unwrap(), SplitSelectionsInto4s.into()),
|
||||
("5".try_into().unwrap(), SplitSelectionsInto5s.into()),
|
||||
("6".try_into().unwrap(), SplitSelectionsInto6s.into()),
|
||||
("7".try_into().unwrap(), SplitSelectionsInto7s.into()),
|
||||
("8".try_into().unwrap(), SplitSelectionsInto8s.into()),
|
||||
("9".try_into().unwrap(), SplitSelectionsInto9s.into()),
|
||||
|
||||
("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()),
|
||||
("A-J".try_into().unwrap(), JumpToSelectedOffset.into()),
|
||||
|
||||
("m".try_into().unwrap(), ToggleMark.into()),
|
||||
|
||||
("y".try_into().unwrap(), Yank.into()),
|
||||
|
||||
("C- ".try_into().unwrap(), InspectSelection.into()),
|
||||
("A- ".try_into().unwrap(), InspectSelectionColor.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::View), [
|
||||
("z".try_into().unwrap(), AlignViewCenter.into()),
|
||||
("b".try_into().unwrap(), AlignViewBottom.into()),
|
||||
("t".try_into().unwrap(), AlignViewTop.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Space), [
|
||||
("w".try_into().unwrap(), Save.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Repeat), [
|
||||
("i".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("k".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("j".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("l".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("down".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("left".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("right".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), ExtendNextWordStart.into()),
|
||||
("e".try_into().unwrap(), ExtendNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), ExtendPreviousWordStart.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::To), [
|
||||
("m".try_into().unwrap(), ExtendToMark.into()),
|
||||
("0".try_into().unwrap(), ExtendToNull.into()),
|
||||
("f".try_into().unwrap(), ExtendToFF.into()),
|
||||
].into()),
|
||||
].into())
|
||||
].into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
use crate::{action::{AppAction, BufferAction, CursorAction}, buffer::{Mode, PartialAction}, config::Config};
|
||||
|
||||
impl Default for Config {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn default() -> Self {
|
||||
use AppAction::*;
|
||||
use BufferAction::*;
|
||||
use CursorAction::*;
|
||||
|
||||
[
|
||||
(Mode::Normal, [
|
||||
(None, [
|
||||
("q".try_into().unwrap(), QuitIfSaved.into()),
|
||||
("Q".try_into().unwrap(), Quit.into()),
|
||||
|
||||
("v".try_into().unwrap(), SelectMode.into()),
|
||||
|
||||
("g".try_into().unwrap(), Goto.into()),
|
||||
("z".try_into().unwrap(), View.into()),
|
||||
("r".try_into().unwrap(), Replace.into()),
|
||||
(" ".try_into().unwrap(), Space.into()),
|
||||
("*".try_into().unwrap(), Repeat.into()),
|
||||
("t".try_into().unwrap(), To.into()),
|
||||
|
||||
("i".try_into().unwrap(), MoveByteUp.into()),
|
||||
("k".try_into().unwrap(), MoveByteDown.into()),
|
||||
("j".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("l".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), MoveByteUp.into()),
|
||||
("down".try_into().unwrap(), MoveByteDown.into()),
|
||||
("left".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("right".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("G".try_into().unwrap(), GotoFileEnd.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), MoveNextWordStart.into()),
|
||||
("e".try_into().unwrap(), MoveNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), MovePreviousWordStart.into()),
|
||||
|
||||
(";".try_into().unwrap(), CollapseSelection.into()),
|
||||
("A-;".try_into().unwrap(), FlipSelections.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("u".try_into().unwrap(), Undo.into()),
|
||||
("U".try_into().unwrap(), Redo.into()),
|
||||
|
||||
("C-j".try_into().unwrap(), PreviousBuffer.into()),
|
||||
("C-l".try_into().unwrap(), NextBuffer.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
|
||||
("(".try_into().unwrap(), RotateSelectionsBackward.into()),
|
||||
(")".try_into().unwrap(), RotateSelectionsForward.into()),
|
||||
|
||||
(",".try_into().unwrap(), KeepPrimarySelection.into()),
|
||||
("A-,".try_into().unwrap(), RemovePrimarySelection.into()),
|
||||
|
||||
("1".try_into().unwrap(), SplitSelectionsInto1s.into()),
|
||||
("2".try_into().unwrap(), SplitSelectionsInto2s.into()),
|
||||
("3".try_into().unwrap(), SplitSelectionsInto3s.into()),
|
||||
("4".try_into().unwrap(), SplitSelectionsInto4s.into()),
|
||||
("5".try_into().unwrap(), SplitSelectionsInto5s.into()),
|
||||
("6".try_into().unwrap(), SplitSelectionsInto6s.into()),
|
||||
("7".try_into().unwrap(), SplitSelectionsInto7s.into()),
|
||||
("8".try_into().unwrap(), SplitSelectionsInto8s.into()),
|
||||
("9".try_into().unwrap(), SplitSelectionsInto9s.into()),
|
||||
|
||||
("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()),
|
||||
("A-J".try_into().unwrap(), JumpToSelectedOffset.into()),
|
||||
|
||||
("m".try_into().unwrap(), ToggleMark.into()),
|
||||
|
||||
("y".try_into().unwrap(), Yank.into()),
|
||||
|
||||
("C- ".try_into().unwrap(), InspectSelection.into()),
|
||||
("A- ".try_into().unwrap(), InspectSelectionColor.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Goto), [
|
||||
("j".try_into().unwrap(), GotoLineStart.into()),
|
||||
("l".try_into().unwrap(), GotoLineEnd.into()),
|
||||
|
||||
("g".try_into().unwrap(), GotoFileStart.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::View), [
|
||||
("z".try_into().unwrap(), AlignViewCenter.into()),
|
||||
("b".try_into().unwrap(), AlignViewBottom.into()),
|
||||
("t".try_into().unwrap(), AlignViewTop.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Space), [
|
||||
("w".try_into().unwrap(), Save.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Repeat), [
|
||||
("i".try_into().unwrap(), MoveByteUp.into()),
|
||||
("k".try_into().unwrap(), MoveByteDown.into()),
|
||||
("j".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("l".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), MoveByteUp.into()),
|
||||
("down".try_into().unwrap(), MoveByteDown.into()),
|
||||
("left".try_into().unwrap(), MoveByteLeft.into()),
|
||||
("right".try_into().unwrap(), MoveByteRight.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), MoveNextWordStart.into()),
|
||||
("e".try_into().unwrap(), MoveNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), MovePreviousWordStart.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::To), [
|
||||
("m".try_into().unwrap(), ExtendToMark.into()),
|
||||
("0".try_into().unwrap(), ExtendToNull.into()),
|
||||
("f".try_into().unwrap(), ExtendToFF.into()),
|
||||
].into()),
|
||||
].into()),
|
||||
(Mode::Select, [
|
||||
(None, [
|
||||
("q".try_into().unwrap(), QuitIfSaved.into()),
|
||||
("Q".try_into().unwrap(), Quit.into()),
|
||||
|
||||
("v".try_into().unwrap(), NormalMode.into()),
|
||||
|
||||
("g".try_into().unwrap(), Goto.into()),
|
||||
("z".try_into().unwrap(), View.into()),
|
||||
("r".try_into().unwrap(), Replace.into()),
|
||||
(" ".try_into().unwrap(), Space.into()),
|
||||
("*".try_into().unwrap(), Repeat.into()),
|
||||
("t".try_into().unwrap(), To.into()),
|
||||
|
||||
("i".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("k".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("j".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("l".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("down".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("left".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("right".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), ExtendNextWordStart.into()),
|
||||
("e".try_into().unwrap(), ExtendNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), ExtendPreviousWordStart.into()),
|
||||
|
||||
(";".try_into().unwrap(), CollapseSelection.into()),
|
||||
("A-;".try_into().unwrap(), FlipSelections.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("u".try_into().unwrap(), Undo.into()),
|
||||
("U".try_into().unwrap(), Redo.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
|
||||
("(".try_into().unwrap(), RotateSelectionsBackward.into()),
|
||||
(")".try_into().unwrap(), RotateSelectionsForward.into()),
|
||||
|
||||
(",".try_into().unwrap(), KeepPrimarySelection.into()),
|
||||
("A-,".try_into().unwrap(), RemovePrimarySelection.into()),
|
||||
|
||||
("1".try_into().unwrap(), SplitSelectionsInto1s.into()),
|
||||
("2".try_into().unwrap(), SplitSelectionsInto2s.into()),
|
||||
("3".try_into().unwrap(), SplitSelectionsInto3s.into()),
|
||||
("4".try_into().unwrap(), SplitSelectionsInto4s.into()),
|
||||
("5".try_into().unwrap(), SplitSelectionsInto5s.into()),
|
||||
("6".try_into().unwrap(), SplitSelectionsInto6s.into()),
|
||||
("7".try_into().unwrap(), SplitSelectionsInto7s.into()),
|
||||
("8".try_into().unwrap(), SplitSelectionsInto8s.into()),
|
||||
("9".try_into().unwrap(), SplitSelectionsInto9s.into()),
|
||||
|
||||
("J".try_into().unwrap(), JumpToSelectedOffsetRelativeToMark.into()),
|
||||
("A-J".try_into().unwrap(), JumpToSelectedOffset.into()),
|
||||
|
||||
("m".try_into().unwrap(), ToggleMark.into()),
|
||||
|
||||
("y".try_into().unwrap(), Yank.into()),
|
||||
|
||||
("C- ".try_into().unwrap(), InspectSelection.into()),
|
||||
("A- ".try_into().unwrap(), InspectSelectionColor.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::View), [
|
||||
("z".try_into().unwrap(), AlignViewCenter.into()),
|
||||
("b".try_into().unwrap(), AlignViewBottom.into()),
|
||||
("t".try_into().unwrap(), AlignViewTop.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Space), [
|
||||
("w".try_into().unwrap(), Save.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::Repeat), [
|
||||
("i".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("k".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("j".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("l".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("up".try_into().unwrap(), ExtendByteUp.into()),
|
||||
("down".try_into().unwrap(), ExtendByteDown.into()),
|
||||
("left".try_into().unwrap(), ExtendByteLeft.into()),
|
||||
("right".try_into().unwrap(), ExtendByteRight.into()),
|
||||
|
||||
("C-e".try_into().unwrap(), ScrollDown.into()),
|
||||
("C-y".try_into().unwrap(), ScrollUp.into()),
|
||||
|
||||
("C-d".try_into().unwrap(), PageCursorHalfDown.into()),
|
||||
("C-u".try_into().unwrap(), PageCursorHalfUp.into()),
|
||||
|
||||
("C-f".try_into().unwrap(), PageDown.into()),
|
||||
("C-b".try_into().unwrap(), PageUp.into()),
|
||||
|
||||
("w".try_into().unwrap(), ExtendNextWordStart.into()),
|
||||
("e".try_into().unwrap(), ExtendNextWordEnd.into()),
|
||||
("b".try_into().unwrap(), ExtendPreviousWordStart.into()),
|
||||
|
||||
("x".try_into().unwrap(), ExtendLineBelow.into()),
|
||||
("X".try_into().unwrap(), ExtendLineAbove.into()),
|
||||
|
||||
("d".try_into().unwrap(), Delete.into()),
|
||||
|
||||
("C".try_into().unwrap(), CopySelectionOnNextLine.into()),
|
||||
].into()),
|
||||
(Some(PartialAction::To), [
|
||||
("m".try_into().unwrap(), ExtendToMark.into()),
|
||||
("0".try_into().unwrap(), ExtendToNull.into()),
|
||||
("f".try_into().unwrap(), ExtendToFF.into()),
|
||||
].into()),
|
||||
].into())
|
||||
].into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
impl Cursor {
|
||||
pub fn execute(
|
||||
&mut self,
|
||||
action: CursorAction,
|
||||
max_contents_index: usize
|
||||
) {
|
||||
match action {
|
||||
CursorAction::MoveByteUp => self.move_byte_up(),
|
||||
CursorAction::MoveByteDown => self.move_byte_down(max_contents_index),
|
||||
CursorAction::MoveByteLeft => self.move_byte_left(),
|
||||
CursorAction::MoveByteRight => self.move_byte_right(max_contents_index),
|
||||
|
||||
CursorAction::ExtendByteUp => self.extend_byte_up(),
|
||||
CursorAction::ExtendByteDown => self.extend_byte_down(max_contents_index),
|
||||
CursorAction::ExtendByteLeft => self.extend_byte_left(),
|
||||
CursorAction::ExtendByteRight => self.extend_byte_right(max_contents_index),
|
||||
|
||||
CursorAction::GotoLineStart => self.goto_line_start(),
|
||||
CursorAction::GotoLineEnd => self.goto_line_end(max_contents_index),
|
||||
CursorAction::GotoFileStart => self.goto_file_start(),
|
||||
CursorAction::GotoFileEnd => self.goto_file_end(max_contents_index),
|
||||
|
||||
CursorAction::MoveNextWordStart => self.move_next_word_start(max_contents_index),
|
||||
CursorAction::MoveNextWordEnd => self.move_next_word_end(max_contents_index),
|
||||
CursorAction::MovePreviousWordStart => self.move_previous_word_start(),
|
||||
|
||||
CursorAction::ExtendNextWordStart => self.extend_next_word_start(max_contents_index),
|
||||
CursorAction::ExtendNextWordEnd => self.extend_next_word_end(max_contents_index),
|
||||
CursorAction::ExtendPreviousWordStart => self.extend_previous_word_start(),
|
||||
|
||||
CursorAction::ExtendLineBelow => self.extend_line_below(max_contents_index),
|
||||
CursorAction::ExtendLineAbove => self.extend_line_above(max_contents_index),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_up(&mut self) {
|
||||
if self.head >= BYTES_PER_LINE {
|
||||
self.head -= BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_down(&mut self, max: usize) {
|
||||
if max - self.head >= BYTES_PER_LINE {
|
||||
self.head += BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_left(&mut self) {
|
||||
if self.head >= 1 {
|
||||
self.head -= 1;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_right(&mut self, max: usize) {
|
||||
if max - self.head >= 1 {
|
||||
self.head += 1;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_up(&mut self) {
|
||||
if self.head >= BYTES_PER_LINE {
|
||||
self.head -= BYTES_PER_LINE;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_down(&mut self, max: usize) {
|
||||
if max - self.head >= BYTES_PER_LINE {
|
||||
self.head += BYTES_PER_LINE;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_left(&mut self) {
|
||||
if self.head >= 1 {
|
||||
self.head -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_right(&mut self, max: usize) {
|
||||
if max - self.head >= 1 {
|
||||
self.head += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn goto_line_start(&mut self) {
|
||||
self.head -= self.head % BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub fn goto_line_end(&mut self, max: usize) {
|
||||
self.head = min(
|
||||
self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE),
|
||||
max
|
||||
);
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub const fn goto_file_start(&mut self) {
|
||||
self.head %= BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub const fn goto_file_end(&mut self, max: usize) {
|
||||
self.head += previous_multiple_of(BYTES_PER_LINE, max + 1 - self.head);
|
||||
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub fn move_next_word_start(&mut self, max: usize) {
|
||||
if self.head == max { return; }
|
||||
|
||||
if self.head.is_multiple_of(4) { // at the beginning of a word
|
||||
self.head = (self.head + 4).min(max);
|
||||
} else {
|
||||
self.head = self.head.next_multiple_of(4).min(max);
|
||||
}
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub fn move_next_word_end(&mut self, max: usize) {
|
||||
if self.head == max { return; }
|
||||
|
||||
self.collapse();
|
||||
if self.head % 4 == 3 { // at the end of a word
|
||||
self.tail = self.head + 1;
|
||||
self.head = (self.head + 4).min(max);
|
||||
} else {
|
||||
self.head = ((self.head + 1).next_multiple_of(4) - 1).min(max);
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_previous_word_start(&mut self) {
|
||||
if self.head == 0 { return; }
|
||||
|
||||
self.collapse();
|
||||
if self.head.is_multiple_of(4) { // at the beginning of a word
|
||||
self.tail = self.head - 1;
|
||||
self.head -= 4;
|
||||
} else {
|
||||
self.head -= self.head % 4;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_next_word_start(&mut self, max: usize) {
|
||||
if self.head == max { return; }
|
||||
|
||||
if self.head.is_multiple_of(4) { // at the beginning of a word
|
||||
self.head = (self.head + 4).min(max);
|
||||
} else {
|
||||
self.head = self.head.next_multiple_of(4).min(max);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_next_word_end(&mut self, max: usize) {
|
||||
if self.head == max { return; }
|
||||
|
||||
if self.head % 4 == 3 { // at the end of a word
|
||||
self.head = (self.head + 4).min(max);
|
||||
} else {
|
||||
self.head = ((self.head + 1).next_multiple_of(4) - 1).min(max);
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_previous_word_start(&mut self) {
|
||||
if self.head == 0 { return; }
|
||||
|
||||
if self.head.is_multiple_of(4) { // at the beginning of a word
|
||||
self.head -= 4;
|
||||
} else {
|
||||
self.head -= self.head % 4;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_line_below(&mut self, max: usize) {
|
||||
if self.tail > self.head {
|
||||
swap(&mut self.head, &mut self.tail);
|
||||
}
|
||||
|
||||
if self.tail.is_multiple_of(BYTES_PER_LINE) &&
|
||||
self.head % BYTES_PER_LINE == BYTES_PER_LINE - 1
|
||||
{
|
||||
self.head = min(self.head + BYTES_PER_LINE, max);
|
||||
} else {
|
||||
self.tail -= self.tail % BYTES_PER_LINE;
|
||||
self.head = min(
|
||||
self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE),
|
||||
max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_line_above(&mut self, max: usize) {
|
||||
if self.head > self.tail {
|
||||
swap(&mut self.head, &mut self.tail);
|
||||
}
|
||||
|
||||
if self.head.is_multiple_of(BYTES_PER_LINE) &&
|
||||
(self.tail % BYTES_PER_LINE == BYTES_PER_LINE - 1 ||
|
||||
self.tail == max)
|
||||
{
|
||||
self.head = self.head.saturating_sub(BYTES_PER_LINE);
|
||||
} else {
|
||||
self.head -= self.head % BYTES_PER_LINE;
|
||||
self.tail = min(
|
||||
self.tail + BYTES_PER_LINE - 1 - (self.tail % BYTES_PER_LINE),
|
||||
max
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn previous_multiple_of(multiple: usize, number: usize) -> usize {
|
||||
if number == 0 {
|
||||
0
|
||||
} else {
|
||||
(number - 1) - ((number - 1) % multiple)
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use crate::cursor::Cursor;
|
||||
|
||||
#[test]
|
||||
fn next_word() {
|
||||
// [a]bcd efgh -> abcd [e]fgh
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_start(99);
|
||||
assert_eq!(cursor, Cursor::at(4));
|
||||
|
||||
// a[b]cd efgh -> abcd [e]fgh
|
||||
let mut cursor = Cursor::at(1);
|
||||
cursor.move_next_word_start(99);
|
||||
assert_eq!(cursor, Cursor::at(4));
|
||||
|
||||
// ab[c]d efgh -> abcd [e]fgh
|
||||
let mut cursor = Cursor::at(2);
|
||||
cursor.move_next_word_start(99);
|
||||
assert_eq!(cursor, Cursor::at(4));
|
||||
|
||||
// abc[d] efgh -> abcd [e]fgh
|
||||
let mut cursor = Cursor::at(3);
|
||||
cursor.move_next_word_start(99);
|
||||
assert_eq!(cursor, Cursor::at(4));
|
||||
|
||||
// [a]bcd -> abc[d]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_start(3);
|
||||
assert_eq!(cursor, Cursor::at(3));
|
||||
|
||||
// [a]bc -> ab[c]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_start(2);
|
||||
assert_eq!(cursor, Cursor::at(2));
|
||||
|
||||
// [a]b -> a[b]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_start(1);
|
||||
assert_eq!(cursor, Cursor::at(1));
|
||||
|
||||
// [a] -> [a]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_start(0);
|
||||
assert_eq!(cursor, Cursor::at(0));
|
||||
|
||||
// ab[c]d -> abc[d]
|
||||
let mut cursor = Cursor::at(2);
|
||||
cursor.move_next_word_start(3);
|
||||
assert_eq!(cursor, Cursor::at(3));
|
||||
|
||||
// abc[d] -> abc[d]
|
||||
let mut cursor = Cursor::at(3);
|
||||
cursor.move_next_word_start(3);
|
||||
assert_eq!(cursor, Cursor::at(3));
|
||||
|
||||
// ab[c[d] -> ab[c[d]
|
||||
let mut cursor = Cursor { tail: 2, head: 3 };
|
||||
cursor.move_next_word_start(3);
|
||||
assert_eq!(cursor, Cursor { tail: 2, head: 3 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_end() {
|
||||
// [a]bcd -> [abcd]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 0, head: 3 });
|
||||
|
||||
// a[b]cd -> [abcd]
|
||||
let mut cursor = Cursor::at(1);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 1, head: 3 });
|
||||
|
||||
// ab[c]d -> [abcd]
|
||||
let mut cursor = Cursor::at(2);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 2, head: 3 });
|
||||
|
||||
// abc[d] efgh -> abcd [efgh]
|
||||
let mut cursor = Cursor::at(3);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 4, head: 7 });
|
||||
|
||||
// abcd [e]fgh -> abcd [efgh]
|
||||
let mut cursor = Cursor::at(4);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 4, head: 7 });
|
||||
|
||||
// abcd e[f]gh -> abcd e[fgh]
|
||||
let mut cursor = Cursor::at(5);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 5, head: 7 });
|
||||
|
||||
// abcd ef[g]h -> abcd ef[gh]
|
||||
let mut cursor = Cursor::at(6);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 6, head: 7 });
|
||||
|
||||
// abcd efg[h] ijkl -> abcd efgh [ijkl]
|
||||
let mut cursor = Cursor::at(7);
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 8, head: 11 });
|
||||
|
||||
// abcd efg[h] -> abcd efg[h]
|
||||
let mut cursor = Cursor::at(7);
|
||||
cursor.move_next_word_end(7);
|
||||
assert_eq!(cursor, Cursor { tail: 7, head: 7 });
|
||||
|
||||
// abcd e[fgh] -> abcd e[fgh]
|
||||
let mut cursor = Cursor { tail: 5, head: 7 };
|
||||
cursor.move_next_word_end(7);
|
||||
assert_eq!(cursor, Cursor { tail: 5, head: 7 });
|
||||
|
||||
// a[b]c -> a[bc]
|
||||
let mut cursor = Cursor::at(1);
|
||||
cursor.move_next_word_end(2);
|
||||
assert_eq!(cursor, Cursor { tail: 1, head: 2 });
|
||||
|
||||
// a[bc] -> a[bc]
|
||||
let mut cursor = Cursor { tail: 1, head: 2};
|
||||
cursor.move_next_word_end(2);
|
||||
assert_eq!(cursor, Cursor { tail: 1, head: 2 });
|
||||
|
||||
// a[b] -> a[b]
|
||||
let mut cursor = Cursor::at(1);
|
||||
cursor.move_next_word_end(1);
|
||||
assert_eq!(cursor, Cursor::at(1));
|
||||
|
||||
// [a]b -> [ab]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_end(1);
|
||||
assert_eq!(cursor, Cursor { tail: 0, head: 1 });
|
||||
|
||||
// [ab] -> [ab]
|
||||
let mut cursor = Cursor { tail: 0, head: 1};
|
||||
cursor.move_next_word_end(1);
|
||||
assert_eq!(cursor, Cursor { tail: 0, head: 1 });
|
||||
|
||||
// [a] -> [a]
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_next_word_end(0);
|
||||
assert_eq!(cursor, Cursor::at(0));
|
||||
|
||||
// [a]bcd] -> [abc[d]
|
||||
let mut cursor = Cursor { head: 0, tail: 3 };
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 0, head: 3 });
|
||||
|
||||
// [a[b]cd -> a[bc[d]
|
||||
let mut cursor = Cursor { tail: 0, head: 1 };
|
||||
cursor.move_next_word_end(99);
|
||||
assert_eq!(cursor, Cursor { tail: 1, head: 3 });
|
||||
|
||||
// abc[d] ef -> abcd [ef]
|
||||
let mut cursor = Cursor::at(3);
|
||||
cursor.move_next_word_end(5);
|
||||
assert_eq!(cursor, Cursor { tail: 4, head: 5 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previous_beginning() {
|
||||
// abcd efgh [i]jkl -> abcd [efgh] ijkl
|
||||
let mut cursor = Cursor::at(8);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 4, tail: 7 });
|
||||
|
||||
// abcd efg[h] -> abcd [efgh]
|
||||
let mut cursor = Cursor::at(7);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 4, tail: 7 });
|
||||
|
||||
// abcd ef[g]h -> abcd [efg]h
|
||||
let mut cursor = Cursor::at(6);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 4, tail: 6 });
|
||||
|
||||
// abcd e[f]gh -> abcd [ef]gh
|
||||
let mut cursor = Cursor::at(5);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 4, tail: 5 });
|
||||
|
||||
// abcd [e]fgh -> [abcd] efgh
|
||||
let mut cursor = Cursor::at(4);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 3 });
|
||||
|
||||
// abc[d] -> [abcd]
|
||||
let mut cursor = Cursor::at(3);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 3 });
|
||||
|
||||
// ab[c]d -> [abc]d
|
||||
let mut cursor = Cursor::at(2);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 2 });
|
||||
|
||||
// a[b]cd -> [ab]cd
|
||||
let mut cursor = Cursor::at(1);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 1 });
|
||||
|
||||
// [a]bcd -> [a]bcd
|
||||
let mut cursor = Cursor::at(0);
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 0 });
|
||||
|
||||
// [abc[d] -> [a]bcd]
|
||||
let mut cursor = Cursor { tail: 0, head: 3 };
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 3 });
|
||||
|
||||
// ab[c]d] -> [a]bc]d
|
||||
let mut cursor = Cursor { head: 2, tail: 3 };
|
||||
cursor.move_previous_word_start();
|
||||
assert_eq!(cursor, Cursor { head: 0, tail: 2 });
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use std::{cmp::min, convert::identity, iter};
|
||||
use crate::{app::WindowSize, buffer::Buffer, cursor::Cursor};
|
||||
use crate::{window_size::WindowSize, buffer::Buffer, cursor::Cursor};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EditAction {
|
||||
|
||||
+13
-3
@@ -6,15 +6,20 @@
|
||||
#![feature(hash_set_entry)]
|
||||
#![feature(trim_prefix_suffix)]
|
||||
|
||||
use arguments::Arguments;
|
||||
use clap::Parser;
|
||||
use app::App;
|
||||
use crossterm::{QueueableCommand, event::{DisableMouseCapture, EnableMouseCapture}};
|
||||
|
||||
mod app;
|
||||
mod buffer;
|
||||
mod popup;
|
||||
mod config;
|
||||
mod cursor;
|
||||
mod action;
|
||||
mod edit_action;
|
||||
mod arguments;
|
||||
mod window_size;
|
||||
|
||||
mod cardinality;
|
||||
mod empty_span;
|
||||
@@ -28,14 +33,13 @@ const LINES_OF_PADDING: usize = 5;
|
||||
const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE;
|
||||
|
||||
// TODO:
|
||||
// - help (use clap?)
|
||||
// - clean up files
|
||||
// - update showcase
|
||||
// - write docs
|
||||
// - simonomi.dev/hexapoda?
|
||||
// - config
|
||||
// - schema!!
|
||||
// - uhhhhh?
|
||||
// - fix scroll clamping
|
||||
// - inspector translations for varint
|
||||
// - search
|
||||
// - ascii and bytes (`/` and `A-/`?)
|
||||
@@ -65,7 +69,13 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE;
|
||||
// - how to fit??! `-128` longer than `80`
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
let arguments = Arguments::parse();
|
||||
|
||||
let mut app = App::new(
|
||||
arguments.config,
|
||||
&arguments.files
|
||||
);
|
||||
|
||||
let mut terminal = ratatui::init();
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
terminal.backend_mut().queue(EnableMouseCapture).unwrap();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
use ratatui::{layout::{Constraint, Rect}, style::{Style, Stylize}, text::Span, widgets::{Block, Borders, Clear, Widget}};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Popup {
|
||||
pub at: usize,
|
||||
width: u16,
|
||||
primary: bool,
|
||||
lines: Vec<Span<'static>>
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
pub fn new(at: usize, lines: Vec<Span<'static>>) -> Self {
|
||||
Self {
|
||||
at,
|
||||
width: lines
|
||||
.iter()
|
||||
.map(|line| line.width() as u16)
|
||||
.max()
|
||||
.unwrap_or(0),
|
||||
primary: false,
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn area_at(&self, x: u16, y: u16) -> Rect {
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width: self.width + 2,
|
||||
height: self.lines.len() as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub const fn as_primary(mut self) -> Self {
|
||||
self.primary = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Popup {
|
||||
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) {
|
||||
Clear.render(area, buf);
|
||||
|
||||
let border_color = if self.primary {
|
||||
Style::new().white()
|
||||
} else {
|
||||
Style::new().gray()
|
||||
};
|
||||
|
||||
Block::new()
|
||||
.on_dark_gray()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_style(border_color)
|
||||
.render(area, buf);
|
||||
|
||||
for (line, area) in self.lines.iter().zip(area.rows()) {
|
||||
line.render(
|
||||
area.centered_horizontally(Constraint::Length(line.width() as u16)),
|
||||
buf
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::BYTES_PER_LINE;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct WindowSize {
|
||||
pub rows: usize,
|
||||
pub covered_rows: usize,
|
||||
}
|
||||
|
||||
impl WindowSize {
|
||||
pub const fn visible_byte_count(&self) -> usize {
|
||||
self.hex_rows() * BYTES_PER_LINE
|
||||
}
|
||||
|
||||
pub const fn hex_rows(&self) -> usize {
|
||||
self.rows - self.covered_rows
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user