Open ellie opened 1 year ago
so taking your idea of syntect and just doing a copypasta'd poc...
the syntax is defined statically, the theme... but fairly simple to integrate without much consideration about anything else lol...
the changed file so you can mess with...
use std::time::Duration;
use crate::tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, StatefulWidget, Widget},
};
use atuin_client::history::History;
// use crossterm::style::Color;
use syntect::easy::HighlightLines;
use syntect::parsing::SyntaxSet;
use syntect::highlighting::{ThemeSet, Style as SynStyle};
use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
use super::format_duration;
pub struct HistoryList<'a> {
history: &'a [History],
block: Option<Block<'a>>,
}
#[derive(Default)]
pub struct ListState {
offset: usize,
selected: usize,
max_entries: usize,
}
impl ListState {
pub fn selected(&self) -> usize {
self.selected
}
pub fn max_entries(&self) -> usize {
self.max_entries
}
pub fn select(&mut self, index: usize) {
self.selected = index;
}
}
impl<'a> StatefulWidget for HistoryList<'a> {
type State = ListState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let list_area = self.block.take().map_or(area, |b| {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
});
if list_area.width < 1 || list_area.height < 1 || self.history.is_empty() {
return;
}
let list_height = list_area.height as usize;
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
state.offset = start;
state.max_entries = end - start;
let mut s = DrawState {
buf,
list_area,
x: 0,
y: 0,
state,
};
for item in self.history.iter().skip(state.offset).take(end - start) {
s.index();
s.duration(item);
s.time(item);
s.command(item);
// reset line
s.y += 1;
s.x = 0;
}
}
}
impl<'a> HistoryList<'a> {
pub fn new(history: &'a [History]) -> Self {
Self {
history,
block: None,
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
fn get_items_bounds(&self, selected: usize, offset: usize, height: usize) -> (usize, usize) {
let offset = offset.min(self.history.len().saturating_sub(1));
let max_scroll_space = height.min(10);
if offset + height < selected + max_scroll_space {
let end = selected + max_scroll_space;
(end - height, end)
} else if selected < offset {
(selected, selected + height)
} else {
(offset, offset + height)
}
}
}
struct DrawState<'a> {
buf: &'a mut Buffer,
list_area: Rect,
x: u16,
y: u16,
state: &'a ListState,
}
// longest line prefix I could come up with
#[allow(clippy::cast_possible_truncation)] // we know that this is <65536 length
pub const PREFIX_LENGTH: u16 = " > 123ms 59s ago".len() as u16;
impl DrawState<'_> {
fn index(&mut self) {
// these encode the slices of `" > "`, `" {n} "`, or `" "` in a compact form.
// Yes, this is a hack, but it makes me feel happy
static SLICES: &str = " > 1 2 3 4 5 6 7 8 9 ";
let i = self.y as usize + self.state.offset;
let i = i.checked_sub(self.state.selected);
let i = i.unwrap_or(10).min(10) * 2;
self.draw(&SLICES[i..i + 3], Style::default());
}
fn duration(&mut self, h: &History) {
let status = Style::default().fg(if h.success() {
Color::Green
} else {
Color::Red
});
let duration = Duration::from_nanos(u64::try_from(h.duration).unwrap_or(0));
self.draw(&format_duration(duration), status);
}
#[allow(clippy::cast_possible_truncation)] // we know that time.len() will be <6
fn time(&mut self, h: &History) {
let style = Style::default().fg(Color::Blue);
// Account for the chance that h.timestamp is "in the future"
// This would mean that "since" is negative, and the unwrap here
// would fail.
// If the timestamp would otherwise be in the future, display
// the time since as 0.
let since = chrono::Utc::now() - h.timestamp;
let time = format_duration(since.to_std().unwrap_or_default());
// pad the time a little bit before we write. this aligns things nicely
self.x = PREFIX_LENGTH - 4 - time.len() as u16;
self.draw(&time, style);
self.draw(" ago", style);
}
fn command(&mut self, h: &History) {
let mut style = Style::default();
if self.y as usize + self.state.offset == self.state.selected {
style = style.fg(Color::Red).add_modifier(Modifier::BOLD);
}
// Load these once at the start of your program
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let syntax = ps.find_syntax_by_extension("zsh").unwrap();
let mut hi = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]);
for line in LinesWithEndings::from(&h.command.as_str()) {
let ranges: Vec<(SynStyle, &str)> = hi.highlight_line(line, &ps).unwrap();
self.x += 1;
for sec in ranges {
// Color::
let fg = Color::Rgb { 0: sec.0.foreground.r, 1: sec.0.foreground.g, 2: sec.0.foreground.b };
let bg = Color::Rgb { 0: sec.0.background.r, 1: sec.0.background.g, 2: sec.0.background.b };
// style.fg(fg).bg(bg); // .add_modifier(sec.0.font_style);
self.draw(sec.1, style.fg(fg)); // .bg(bg)
}
// let escaped = as_24_bit_terminal_escaped(&ranges[..], true);
// self.draw(escaped.as_str(), Style::default());
// break
}
// for section in h.command.split_ascii_whitespace() {
// self.x += 1;
// if self.x > self.list_area.width {
// // Avoid attempting to draw a command section beyond the width
// // of the list
// return;
// }
// self.draw(section, style);
// }
}
fn draw(&mut self, s: &str, style: Style) {
let cx = self.list_area.left() + self.x;
let cy = self.list_area.bottom() - self.y - 1;
let w = (self.list_area.width - self.x) as usize;
self.x += self.buf.set_stringn(cx, cy, s, w, style).0 - cx;
}
}
A cool addition to this would be the ability to highlight where in the filtered commands a search token was found. I'm coming from zsh-history-substring-search
which enabled this with the HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND
setting: https://github.com/zsh-users/zsh-history-substring-search#configuration
@lukekarrys @ellie just as something that maybe can be put into wiki or contrib scripts somewhere...
This is FZF searching atuin with the preview being highlighted by bat
.
#!/bin/zsh
emulate zsh
FZF_COLORS="
--color=fg:regular:green:dim,fg+:regular:bright-green:dim
--color=hl:regular:red:dim:bold,hl+:regular:yellow:dim:bold
--color=bg:,bg+:
"
export FZF_DEFAULT_OPTS="
--prompt '❱ '
--pointer '➤'
--marker '┃'
--cycle
$FZF_COLORS
--reverse --min-height 14 --ansi
--bind=ctrl-s:toggle-sort
--bind=alt-p:preview-up,alt-n:preview-down
--bind=ctrl-k:preview-up,ctrl-j:preview-down
--bind=ctrl-u:half-page-up,ctrl-d:half-page-down
--bind=alt-,:first,alt-.:last,change:first"
ZERODIR=$(dirname $0)
0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
0="${${(M)0:#/*}:-$PWD/$0}"
ARGZERO=$0
if (( ZDEBUG )); then
emulate -L zsh -o err_return -o no_unset -o xtrace
print -rn -- $'\e]0;ZDEBUG\a' >$TTY
fi
q=""
typeset -a search=("${ARGZERO}" "searcher")
accept(){
while read datte time dir cmd; do
echo $cmd;
done
}
fzfs(){
(){
cat - | sort | uniq | sort
} < <(${(z)search[@]} ) > >(fzf_list)
}
local atuin="atuin"
local LIMIT=100
searcher(){
typeset -a clipcatsearch=(${atuin} "search" --limit ${LIMIT} --format '{time}\t\t{directory}\t\t{command}')
local q=${1:-}
clipcatsearch+=(${q})
typeset -a lines=()
while read line; do # id ts ts2 size ctype context; do
lines+=($line)
done < <(${clipcatsearch[@]})
print ${(j.\n.)lines[@]}
}
typeset timer=""
previewer(){
{
echo "# TIME=\"${(z)1}\"\tDIR=\"${2}\"\n${3}\n"
}
} > >(bat -f -n -l zsh --wrap character --theme Dracula --terminal-width -2 --file-name "${2}") # --style "snip"
typeset -a binds=("enter:accept-non-empty" "change:reload:(${search} {q})" "ctrl-space:reload:(${search} {q})")
typeset -a previewer=("$ARGZERO" "previewer" \"{1}\" "{2}" "{3}" )
typeset -a bind=('--bind')
fzf_list(){
print -v HEADER -f "Atuin Search"
local -a bindsa=(${bind:^^binds})
local WHFZF="fzf" # "
{
{cat /dev/stdin} | \
${WHFZF} --header "${HEADER}"\
--nth 1.. --with-nth 3.. \
--tabstop=1 --sync \
--info=inline\
--no-sort\
--tac\
--delimiter="\t\t" \
${bindsa} \
--preview-window="down,38%,border-rounded,nofollow,nohidden,~3,+3/2"\
--preview="${previewer}"
} > >(accept)
}
{
typeset -a cmd=()
LISTER=${1:-fzfs}
case "$LISTER" in
accept) echo ${@:2};
;;
searcher) searcher ${@:2};
;;
previewer) previewer ${@:2};
;;
*) fzfs ""
;;
esac
}
Maybe something like Tree-sitter (I've found tree-sitter-highlight - crates.io: Rust Package Registry during a quick search) could be a viable option to evaluate for syntax highlighting as well; the difference is, that this would be a concrete syntax tree based approach to syntax highlighting instead of a regex based one (it may have less performance costs because of this).
There's also a bash grammar availabe for tree-sitter as a crate: tree-sitter-bash - crates.io: Rust Package Registry.
This issue has been mentioned on Atuin Community. There might be relevant details there:
https://forum.atuin.sh/t/handling-control-sequences-in-commands/70/2
It would be awesome if we could syntax highlight shell history in the interactive UI
Something like this might work: https://github.com/trishume/syntect