use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Line;
use ratatui::widgets::{Block, BorderType, List, ListItem, ListState, StatefulWidget, Widget};
use crate::*;
pub(crate) fn enabled() -> bool {
unsafe { global_user_flag("@ztmux-ratatui", true) }
}
fn big_digit(c: char) -> [&'static str; 5] {
match c {
'0' => ["███", "█ █", "█ █", "█ █", "███"],
'1' => [" █", " █", " █", " █", " █"],
'2' => ["███", " █", "███", "█ ", "███"],
'3' => ["███", " █", "███", " █", "███"],
'4' => ["█ █", "█ █", "███", " █", " █"],
'5' => ["███", "█ ", "███", " █", "███"],
'6' => ["███", "█ ", "███", "█ █", "███"],
'7' => ["███", " █", " █", " █", " █"],
'8' => ["███", "█ █", "███", "█ █", "███"],
'9' => ["███", "█ █", "███", " █", "███"],
':' => [" ", " █ ", " ", " █ ", " "],
'A' => ["███", "█ █", "███", "█ █", "█ █"],
'P' => ["███", "█ █", "███", "█ ", "█ "],
'M' => ["█ █", "███", "█ █", "█ █", "█ █"],
_ => [" ", " ", " ", " ", " "],
}
}
pub(crate) unsafe fn draw_clock(wp: *mut window_pane, s: *mut screen) {
unsafe {
let w = crate::screen_size_x(s) as u16;
let h = crate::screen_size_y(s) as u16;
if w < 4 || h < 3 {
return;
}
let style_24h =
crate::options_::options_get_number_((*(*wp).window).options, "clock-mode-style") != 0;
let mut t = libc::time(std::ptr::null_mut());
let tm = libc::localtime(&raw mut t);
let read = |fmt: &std::ffi::CStr| -> String {
let mut b = [0u8; 64];
libc::strftime(b.as_mut_ptr(), b.len(), fmt.as_ptr().cast(), tm);
std::ffi::CStr::from_ptr(b.as_ptr().cast())
.to_string_lossy()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
};
let time_str = read(if style_24h { c"%H:%M" } else { c"%l:%M %p" });
let date = read(c"%a %d %b %Y");
let area = Rect::new(0, 0, w, h);
let mut buf = Buffer::empty(area);
Block::bordered()
.border_type(BorderType::Rounded)
.title(Line::from(format!(" {date} ")).centered())
.render(area, &mut buf);
let glyph_w = 4u16; let total_w = time_str.chars().count() as u16 * glyph_w;
let inner_w = w.saturating_sub(2);
let x0 = area.x + 1 + inner_w.saturating_sub(total_w) / 2;
let y0 = area.y + h.saturating_sub(5) / 2;
for (gi, ch) in time_str.chars().enumerate() {
let rows = big_digit(ch.to_ascii_uppercase());
for (ry, row) in rows.iter().enumerate() {
for (rx, cc) in row.chars().enumerate() {
if cc == ' ' {
continue;
}
let px = x0 + gi as u16 * glyph_w + rx as u16;
let py = y0 + ry as u16;
if px < area.right().saturating_sub(1) && py < area.bottom().saturating_sub(1) {
buf[(px, py)].set_symbol("█").set_fg(Color::Green);
}
}
}
}
let mut ctx = std::mem::MaybeUninit::<screen_write_ctx>::uninit();
screen_write::screen_write_start(ctx.as_mut_ptr(), s);
screen_write::screen_write_clearscreen(ctx.as_mut_ptr(), 8);
blit(ctx.as_mut_ptr(), &buf);
screen_write::screen_write_stop(ctx.as_mut_ptr());
}
}
fn map_color(c: Option<Color>) -> i32 {
match c {
None | Some(Color::Reset) => 8,
Some(Color::Black) => 0,
Some(Color::Red) => 1,
Some(Color::Green) => 2,
Some(Color::Yellow) => 3,
Some(Color::Blue) => 4,
Some(Color::Magenta) => 5,
Some(Color::Cyan) => 6,
Some(Color::Gray) => 7,
Some(Color::DarkGray) => 90,
Some(Color::LightRed) => 91,
Some(Color::LightGreen) => 92,
Some(Color::LightYellow) => 93,
Some(Color::LightBlue) => 94,
Some(Color::LightMagenta) => 95,
Some(Color::LightCyan) => 96,
Some(Color::White) => 97,
Some(Color::Rgb(r, g, b)) => crate::colour::colour_join_rgb(r, g, b),
Some(Color::Indexed(n)) => {
let n = n as i32;
if n < 8 {
n
} else if n < 16 {
90 + (n - 8)
} else {
n | 0x0100_0000 }
}
}
}
fn tmux_colour_to_ratatui(c: i32) -> Option<Color> {
const COLOUR_FLAG_256: i32 = 0x0100_0000;
const COLOUR_FLAG_RGB: i32 = 0x0200_0000;
if c == -1 || c == 8 {
return None; }
if c & COLOUR_FLAG_RGB != 0 {
let (r, g, b) = crate::colour::colour_split_rgb(c);
return Some(Color::Rgb(r, g, b));
}
if c & COLOUR_FLAG_256 != 0 {
return Some(Color::Indexed((c & 0xff) as u8));
}
match c {
0..=7 => Some(Color::Indexed(c as u8)),
90..=97 => Some(Color::Indexed((c - 90 + 8) as u8)), 100..=107 => Some(Color::Indexed((c - 100 + 8) as u8)),
_ => None,
}
}
unsafe fn message_style_colors(c: *mut client) -> (Option<Color>, Option<Color>) {
unsafe {
let s = (*c).session;
if s.is_null() {
return (None, None);
}
let ft = format_create_defaults(null_mut(), c, null_mut(), null_mut(), null_mut());
let mut gc = std::mem::MaybeUninit::<grid_cell>::uninit();
style_apply(gc.as_mut_ptr(), (*s).options, c!("message-style"), ft);
let gc = gc.assume_init();
format_free(ft);
let (mut fg, mut bg) = (gc.fg, gc.bg);
if gc.attr.contains(grid_attr::GRID_ATTR_REVERSE) {
std::mem::swap(&mut fg, &mut bg);
}
(tmux_colour_to_ratatui(fg), tmux_colour_to_ratatui(bg))
}
}
fn map_modifier(m: Modifier) -> grid_attr {
let mut a = grid_attr::empty();
if m.contains(Modifier::BOLD) {
a |= grid_attr::GRID_ATTR_BRIGHT;
}
if m.contains(Modifier::DIM) {
a |= grid_attr::GRID_ATTR_DIM;
}
if m.contains(Modifier::ITALIC) {
a |= grid_attr::GRID_ATTR_ITALICS;
}
if m.contains(Modifier::UNDERLINED) {
a |= grid_attr::GRID_ATTR_UNDERSCORE;
}
if m.intersects(Modifier::SLOW_BLINK | Modifier::RAPID_BLINK) {
a |= grid_attr::GRID_ATTR_BLINK;
}
if m.contains(Modifier::REVERSED) {
a |= grid_attr::GRID_ATTR_REVERSE;
}
if m.contains(Modifier::HIDDEN) {
a |= grid_attr::GRID_ATTR_HIDDEN;
}
if m.contains(Modifier::CROSSED_OUT) {
a |= grid_attr::GRID_ATTR_STRIKETHROUGH;
}
a
}
unsafe fn set_char(gc: *mut grid_cell, sym: &str) {
unsafe {
let bytes = sym.as_bytes();
let n = bytes.len().min(UTF8_SIZE);
if n == 0 {
(*gc).data.data[0] = b' ';
(*gc).data.have = 1;
(*gc).data.size = 1;
(*gc).data.width = 1;
return;
}
let dst = (&raw mut (*gc).data.data).cast::<u8>();
core::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, n);
(*gc).data.have = n as u8;
(*gc).data.size = n as u8;
(*gc).data.width = 1;
}
}
unsafe fn blit(ctx: *mut screen_write_ctx, buf: &Buffer) {
unsafe {
let area = buf.area;
for y in 0..area.height {
for x in 0..area.width {
let cell = &buf[(x, y)];
let st = cell.style();
let mut gc = std::mem::MaybeUninit::<grid_cell>::uninit();
memcpy__(gc.as_mut_ptr(), &raw const GRID_DEFAULT_CELL);
let gc = gc.as_mut_ptr();
set_char(gc, cell.symbol());
(*gc).fg = map_color(st.fg);
(*gc).bg = map_color(st.bg);
(*gc).attr = map_modifier(st.add_modifier);
screen_write::screen_write_cursormove(ctx, x as i32, y as i32, 0);
screen_write::screen_write_cell(ctx, gc);
}
}
}
}
fn strip_markup(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut chars = name.chars().peekable();
while let Some(c) = chars.next() {
if c == '#' && chars.peek() == Some(&'[') {
chars.next();
for d in chars.by_ref() {
if d == ']' {
break;
}
}
} else {
out.push(c);
}
}
out
}
pub(crate) unsafe fn draw(md: *mut menu_data, ctx: *mut screen_write_ctx) {
unsafe {
let s = &raw mut (*md).s;
let w = crate::screen_size_x(s) as u16;
let h = crate::screen_size_y(s) as u16;
if w == 0 || h == 0 {
return;
}
let area = Rect::new(0, 0, w, h);
let mut buf = Buffer::empty(area);
let menu = (*md).menu;
let inner_w = w.saturating_sub(2) as usize;
let mut items: Vec<ListItem> = Vec::with_capacity((*menu).items.len());
for it in &(*menu).items {
let stripped = strip_markup(&it.name);
let name = stripped.trim();
if name.is_empty() {
items.push(
ListItem::new(Line::from("─".repeat(inner_w)))
.style(Style::default().fg(Color::DarkGray)),
);
} else if let Some(rest) = name.strip_prefix('-') {
items.push(
ListItem::new(Line::from(format!(" {}", rest.trim_start())))
.style(Style::default().add_modifier(Modifier::DIM)),
);
} else {
items.push(ListItem::new(Line::from(format!(" {name}"))));
}
}
let title = strip_markup(&(*menu).title);
let block = Block::bordered()
.border_type(BorderType::Rounded)
.title(Line::from(format!(" {title} ")).centered());
let list = List::new(items)
.block(block)
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
let mut state = ListState::default();
if (*md).choice >= 0 {
state.select(Some((*md).choice as usize));
}
StatefulWidget::render(list, area, &mut buf, &mut state);
blit(ctx, &buf);
}
}
unsafe fn blit_tty(tty: *mut tty, buf: &Buffer, px: u32, py: u32) {
unsafe {
let area = buf.area;
for y in 0..area.height {
for x in 0..area.width {
let cell = &buf[(x, y)];
let st = cell.style();
let mut gc = std::mem::MaybeUninit::<grid_cell>::uninit();
memcpy__(gc.as_mut_ptr(), &raw const GRID_DEFAULT_CELL);
let gc = gc.as_mut_ptr();
set_char(gc, cell.symbol());
(*gc).fg = map_color(st.fg);
(*gc).bg = map_color(st.bg);
(*gc).attr = map_modifier(st.add_modifier);
tty_cursor(tty, px + x as u32, py + y as u32);
tty_cell(
tty,
gc,
&raw const GRID_DEFAULT_CELL,
std::ptr::null(),
std::ptr::null_mut(),
);
}
}
}
}
pub(crate) unsafe fn draw_pane_number(ctx: *mut screen_redraw_ctx, wp: *mut window_pane) {
unsafe {
let c = (*ctx).c;
let tty = &raw mut (*c).tty;
if (*wp).xoff < (*ctx).ox
|| (*wp).yoff < (*ctx).oy
|| (*wp).xoff + (*wp).sx > (*ctx).ox + (*ctx).sx
|| (*wp).yoff + (*wp).sy > (*ctx).oy + (*ctx).sy
{
return;
}
let mut idx: u32 = 0;
window_pane_index(wp, &raw mut idx);
let label = format!("{idx}");
let digits = label.chars().count() as u16;
let size = format!("{}x{}", (*wp).sx, (*wp).sy);
let need_w = (digits * 4 + 1).max(size.len() as u16 + 2);
let boxw = need_w.min((*wp).sx as u16);
let boxh = 7u16.min((*wp).sy as u16);
if boxw < 5 || boxh < 5 {
return;
}
let area = Rect::new(0, 0, boxw, boxh);
let mut buf = Buffer::empty(area);
let active = (*(*wp).window).active == wp;
let colour = if active { Color::Green } else { Color::Blue };
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(colour))
.title(Line::from(format!(" {size} ")).centered())
.render(area, &mut buf);
let total_w = digits * 4;
let x0 = 1 + (boxw.saturating_sub(2)).saturating_sub(total_w) / 2;
let y0 = 1 + (boxh.saturating_sub(2)).saturating_sub(5) / 2;
for (gi, ch) in label.chars().enumerate() {
for (ry, row) in big_digit(ch).iter().enumerate() {
for (rx, cc) in row.chars().enumerate() {
if cc == ' ' {
continue;
}
let bx = x0 + gi as u16 * 4 + rx as u16;
let by = y0 + ry as u16;
if bx < boxw - 1 && by < boxh - 1 {
buf[(bx, by)].set_symbol("█").set_fg(colour);
}
}
}
}
let yoff = (*wp).yoff - (*ctx).oy
+ if (*ctx).statustop != 0 {
(*ctx).statuslines
} else {
0
};
let xoff = (*wp).xoff - (*ctx).ox;
let px = xoff + ((*wp).sx.saturating_sub(boxw as u32)) / 2;
let py = yoff + ((*wp).sy.saturating_sub(boxh as u32)) / 2;
blit_tty(tty, &buf, px, py);
}
}
unsafe fn prompt_graphemes(buf: *const utf8_data) -> Vec<(String, u16)> {
unsafe {
let mut out = Vec::new();
if buf.is_null() {
return out;
}
let mut i = 0isize;
loop {
let ud = buf.offset(i);
let size = (*ud).size as usize;
if size == 0 {
break;
}
let bytes = std::slice::from_raw_parts((&raw const (*ud).data).cast::<u8>(), size);
let text = String::from_utf8_lossy(bytes).into_owned();
out.push((text, ((*ud).width as u16).max(1)));
i += 1;
}
out
}
}
static PROMPT_OVERLAY_MARK: u8 = 0;
static PROMPT_SEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
fn prompt_mark() -> *mut core::ffi::c_void {
(&raw const PROMPT_OVERLAY_MARK).cast_mut().cast()
}
pub(crate) unsafe fn set_prompt_overlay(c: *mut client) {
unsafe {
PROMPT_SEL.store(-1, std::sync::atomic::Ordering::Relaxed);
server_client_set_overlay(
c,
0,
Some(prompt_check_cb),
None,
Some(prompt_overlay_draw),
Some(prompt_overlay_key),
None,
None,
prompt_mark(),
);
}
}
pub(crate) unsafe fn clear_prompt_overlay(c: *mut client) {
unsafe {
if (*c).overlay_data == prompt_mark() {
server_client_clear_overlay(c);
}
}
}
static MESSAGE_OVERLAY_MARK: u8 = 0;
fn message_mark() -> *mut core::ffi::c_void {
(&raw const MESSAGE_OVERLAY_MARK).cast_mut().cast()
}
pub(crate) unsafe fn set_message_overlay(c: *mut client) {
unsafe {
server_client_set_overlay(
c,
0,
Some(message_check_cb),
None,
Some(message_overlay_draw),
None,
None,
None,
message_mark(),
);
}
}
pub(crate) unsafe fn clear_message_overlay(c: *mut client) {
unsafe {
if (*c).overlay_data == message_mark() {
server_client_clear_overlay(c);
}
}
}
struct MessageLayout {
px: u16,
py: u16,
w: u16,
h: u16,
lines: Vec<String>,
}
unsafe fn message_layout(c: *mut client) -> Option<MessageLayout> {
unsafe {
if (*c).message_string.is_none() {
return None;
}
let sx = (*c).tty.sx as u16;
let sy = (*c).tty.sy as u16;
if sx < 12 || sy < 4 {
return None;
}
let text = strip_markup(crate::cstr_to_str((*c).message_string_ptr()));
let text = text.replace(['\n', '\r', '\t'], " ");
let text = text.trim();
if text.is_empty() {
return None;
}
let inner = (sx.saturating_sub(6) as usize).clamp(8, 100);
let mut lines: Vec<String> = Vec::new();
let mut cur = String::new();
for word in text.split(' ').filter(|w| !w.is_empty()) {
if word.chars().count() > inner {
if !cur.is_empty() {
lines.push(std::mem::take(&mut cur));
}
let mut chunk = String::new();
for ch in word.chars() {
if chunk.chars().count() >= inner {
lines.push(std::mem::take(&mut chunk));
}
chunk.push(ch);
}
cur = chunk;
continue;
}
let extra = usize::from(!cur.is_empty());
if !cur.is_empty() && cur.chars().count() + extra + word.chars().count() > inner {
lines.push(std::mem::take(&mut cur));
}
if !cur.is_empty() {
cur.push(' ');
}
cur.push_str(word);
}
if !cur.is_empty() {
lines.push(cur);
}
if lines.len() > 6 {
lines.truncate(6);
if let Some(last) = lines.last_mut() {
let mut t: String = last.chars().take(inner.saturating_sub(1)).collect();
t.push('\u{2026}');
*last = t;
}
}
let widest = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0) as u16;
let title_w = " message ".chars().count() as u16;
let w = (widest + 4).max(title_w + 2).min(sx.saturating_sub(2));
let h = lines.len() as u16 + 2;
let py = sy
.saturating_sub(sy / 4)
.saturating_sub(1)
.min(sy.saturating_sub(h));
let px = sx.saturating_sub(w) / 2;
Some(MessageLayout {
px,
py,
w,
h,
lines,
})
}
}
unsafe fn message_check_cb(
c: *mut client,
_data: *mut core::ffi::c_void,
px: u32,
py: u32,
nx: u32,
r: *mut overlay_ranges,
) {
unsafe {
if let Some(l) = message_layout(c) {
server_client_overlay_range(
l.px as u32,
l.py as u32,
l.w as u32,
l.h as u32,
px,
py,
nx,
r,
);
} else {
(*r).px[0] = px;
(*r).nx[0] = nx;
(*r).px[1] = 0;
(*r).nx[1] = 0;
(*r).px[2] = 0;
(*r).nx[2] = 0;
}
}
}
unsafe fn message_overlay_draw(
c: *mut client,
_data: *mut core::ffi::c_void,
_rctx: *mut screen_redraw_ctx,
) {
unsafe {
let Some(l) = message_layout(c) else {
return;
};
let tty = &raw mut (*c).tty;
let area = Rect::new(0, 0, l.w, l.h);
let mut buf = Buffer::empty(area);
let (mfg, mbg) = message_style_colors(c);
let fg = mfg.unwrap_or(Color::Yellow);
let mut box_style = Style::default().fg(fg);
if let Some(bg) = mbg {
box_style = box_style.bg(bg);
}
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(fg))
.style(box_style)
.title(Line::from(" message ").centered())
.render(area, &mut buf);
let mut text_style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
if let Some(bg) = mbg {
text_style = text_style.bg(bg);
}
for (ri, line) in l.lines.iter().enumerate() {
let ry = 1 + ri as u16;
let mut cx = 2u16;
for ch in line.chars() {
if cx >= l.w - 1 {
break;
}
buf[(cx, ry)].set_char(ch).set_style(text_style);
cx += 1;
}
}
blit_tty(tty, &buf, l.px as u32, l.py as u32);
}
}
struct PromptLayout {
px: u16,
py: u16,
w: u16,
h: u16,
plabel: String,
input: Vec<(String, u16)>,
cursor_i: usize,
cands: Vec<String>,
}
unsafe fn prompt_layout(c: *mut client) -> Option<PromptLayout> {
unsafe {
if (*c).prompt_string.is_none() {
return None;
}
let sx = (*c).tty.sx as u16;
let sy = (*c).tty.sy as u16;
if sx < 12 || sy < 6 {
return None;
}
let prompt = strip_markup(crate::cstr_to_str((*c).prompt_string_ptr()));
let input = prompt_graphemes((*c).prompt_buffer);
let cursor_i = (*c).prompt_index;
let input_w: u16 = input.iter().map(|(_, w)| *w).sum();
let plabel = format!("{prompt} ");
let plabel_w = plabel.chars().count() as u16;
let max_rows = 8u16;
let cands = prompt_completions(c);
let py = sy.saturating_sub(sy / 4).saturating_sub(1);
let space_below = sy.saturating_sub(py + 3); let n_rows = (cands.len() as u16).min(max_rows).min(space_below);
let widest = cands
.iter()
.take(n_rows as usize)
.map(|s| s.chars().count())
.max()
.unwrap_or(0) as u16;
let inner = (plabel_w + input_w + 1)
.max(widest + 2)
.max(36)
.min(sx.saturating_sub(4));
let w = inner + 2;
let h = 3 + n_rows;
let px = sx.saturating_sub(w) / 2;
Some(PromptLayout {
px,
py,
w,
h,
plabel,
input,
cursor_i,
cands,
})
}
}
unsafe fn prompt_check_cb(
c: *mut client,
_data: *mut core::ffi::c_void,
px: u32,
py: u32,
nx: u32,
r: *mut overlay_ranges,
) {
unsafe {
if let Some(l) = prompt_layout(c) {
server_client_overlay_range(
l.px as u32,
l.py as u32,
l.w as u32,
l.h as u32,
px,
py,
nx,
r,
);
} else {
(*r).px[0] = px;
(*r).nx[0] = nx;
(*r).px[1] = 0;
(*r).nx[1] = 0;
(*r).px[2] = 0;
(*r).nx[2] = 0;
}
}
}
unsafe fn prompt_overlay_key(
c: *mut client,
_data: *mut core::ffi::c_void,
event: *mut key_event,
) -> i32 {
unsafe {
use std::sync::atomic::Ordering::Relaxed;
let raw = (*event).key;
if KEYC_IS_MOUSE(raw) {
let m = &raw mut (*event).m;
let inside = prompt_layout(c).is_some_and(|l| {
(*m).x >= l.px as u32
&& (*m).x < (l.px + l.w) as u32
&& (*m).y >= l.py as u32
&& (*m).y < (l.py + l.h) as u32
});
if !inside && !MOUSE_RELEASE((*m).b) && !MOUSE_WHEEL((*m).b) && !MOUSE_DRAG((*m).b) {
status_prompt_key(c, b'\x1b' as key_code); }
return 0;
}
let key = raw & !KEYC_MASK_FLAGS; let cands = prompt_completions(c);
let n = cands.len() as i32;
let redraw = |c: *mut client| {
(*c).flags |= client_flag::REDRAWWINDOW | client_flag::REDRAWOVERLAY;
};
if n > 0 && (key == keyc::KEYC_UP as key_code || key == keyc::KEYC_DOWN as key_code) {
let cur = PROMPT_SEL.load(Relaxed);
let next = if key == keyc::KEYC_DOWN as key_code {
(cur + 1).min(n - 1)
} else {
(cur - 1).max(0)
};
PROMPT_SEL.store(next, Relaxed);
redraw(c);
return 0;
}
if key == 0x09 {
if n > 0 {
let idx = PROMPT_SEL.load(Relaxed).max(0) as usize;
if let Some(cand) = cands.get(idx) {
crate::status::status_prompt_replace_complete(c, Some(cand));
}
}
PROMPT_SEL.store(-1, Relaxed);
redraw(c);
return 0;
}
PROMPT_SEL.store(-1, Relaxed);
status_prompt_key(c, raw);
redraw(c);
0
}
}
unsafe fn prompt_completions(c: *mut client) -> Vec<String> {
unsafe {
let input = prompt_graphemes((*c).prompt_buffer);
let cursor_i = (*c).prompt_index;
let before: String = input[..cursor_i.min(input.len())]
.iter()
.map(|(t, _)| t.as_str())
.collect();
let at_start = !before.trim_end().contains(char::is_whitespace);
let word = before.rsplit(char::is_whitespace).next().unwrap_or("");
if word.is_empty() {
return Vec::new();
}
let Ok(cword) = std::ffi::CString::new(word) else {
return Vec::new();
};
crate::status::status_prompt_complete_list(cword.as_ptr().cast(), at_start as i32)
}
}
unsafe fn prompt_overlay_draw(
c: *mut client,
_data: *mut core::ffi::c_void,
_rctx: *mut screen_redraw_ctx,
) {
unsafe {
let Some(l) = prompt_layout(c) else {
return;
};
let tty = &raw mut (*c).tty;
let boxw = l.w;
let boxh = l.h;
let plabel = l.plabel;
let input = l.input;
let cursor_i = l.cursor_i;
let cands = l.cands;
let n_rows = boxh - 3;
let (mfg, mbg) = message_style_colors(c);
let accent = mfg.unwrap_or(Color::Cyan);
let with_bg = |s: Style| match mbg {
Some(bg) => s.bg(bg),
None => s,
};
let area = Rect::new(0, 0, boxw, boxh);
let mut buf = Buffer::empty(area);
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(accent))
.style(with_bg(Style::default().fg(accent)))
.title(Line::from(" command ").centered())
.render(area, &mut buf);
let row = 1u16;
let mut col = 1u16;
for ch in plabel.chars() {
if col >= boxw - 1 {
break;
}
buf[(col, row)].set_char(ch).set_style(with_bg(
Style::default().fg(accent).add_modifier(Modifier::BOLD),
));
col += 1;
}
let start_col = col;
let avail = (boxw - 1).saturating_sub(start_col);
let cursor_col: u16 = input[..cursor_i.min(input.len())]
.iter()
.map(|(_, w)| *w)
.sum();
let off = if cursor_col >= avail {
cursor_col - avail + 1
} else {
0
};
let mut x = 0u16;
for (gi, (text, w)) in input.iter().enumerate() {
if x >= off {
let cx = start_col + (x - off);
if cx < boxw - 1 {
let st = if gi == cursor_i {
with_bg(Style::default()).add_modifier(Modifier::REVERSED)
} else {
with_bg(Style::default())
};
buf[(cx, row)].set_symbol(text).set_style(st);
}
}
x += *w;
}
if cursor_i >= input.len() {
let cx = start_col + x.saturating_sub(off);
if cx < boxw - 1 {
buf[(cx, row)].set_symbol(" ").set_style(
with_bg(Style::default().fg(accent)).add_modifier(Modifier::REVERSED),
);
}
}
let sel = PROMPT_SEL.load(std::sync::atomic::Ordering::Relaxed);
for (i, cand) in cands.iter().take(n_rows as usize).enumerate() {
let ry = 2 + i as u16;
let style = if i as i32 == sel {
with_bg(Style::default().fg(accent))
.add_modifier(Modifier::REVERSED | Modifier::BOLD)
} else {
with_bg(Style::default().fg(Color::Gray))
};
let mut cx = 2u16;
for ch in cand.chars() {
if cx >= boxw - 1 {
break;
}
buf[(cx, ry)].set_char(ch).set_style(style);
cx += 1;
}
}
blit_tty(tty, &buf, l.px as u32, l.py as u32);
}
}
unsafe fn hint_is_ours(c: *mut client) -> bool {
unsafe {
(*c).overlay_draw
.is_some_and(|f| std::ptr::fn_addr_eq(f, hint_draw as OverlayDrawFn))
}
}
type OverlayDrawFn = unsafe fn(*mut client, *mut core::ffi::c_void, *mut screen_redraw_ctx);
unsafe fn hint_free(_c: *mut client, data: *mut core::ffi::c_void) {
unsafe {
if !data.is_null() {
drop(Box::from_raw(data.cast::<HintLayout>()));
}
}
}
unsafe fn hint_bindings(c: *mut client) -> Vec<(String, String)> {
unsafe {
let table = (*c).keytable;
let mut out = Vec::new();
if table.is_null() {
return out;
}
let mut bd = key_bindings_first(table);
while !bd.is_null() {
let key = (*bd).key;
let note = (*bd).note_ptr();
let (label, owned) = if !note.is_null() && *note != 0 {
(note.cast_mut(), false)
} else if !(*bd).cmdlist.is_null() {
(cmd_list_print(&*(*bd).cmdlist, 1), true)
} else {
(null_mut(), false)
};
if !KEYC_IS_MOUSE(key) && !label.is_null() && *label != 0 {
let ks = key_string_lookup_key(key, 0);
if !ks.is_null() {
let keystr = crate::cstr_to_str(ks).to_string();
let mut notestr = crate::cstr_to_str(label).to_string();
if owned && notestr.chars().count() > 24 {
notestr = notestr.chars().take(23).collect::<String>() + "\u{2026}";
}
out.push((keystr, notestr));
}
}
if owned && !label.is_null() {
crate::free_(label);
}
bd = key_bindings_next(table, bd);
}
out
}
}
struct HintLayout {
px: u16,
py: u16,
w: u16,
h: u16,
table: String,
rows: Vec<Vec<(String, String)>>,
}
unsafe fn hint_layout(c: *mut client) -> Option<HintLayout> {
unsafe {
if (*c).keytable.is_null() {
return None;
}
let sx = (*c).tty.sx as u16;
let sy = (*c).tty.sy as u16;
if sx < 24 || sy < 6 {
return None;
}
let chips = hint_bindings(c);
if chips.is_empty() {
return None;
}
let table = strip_markup(crate::cstr_to_str((*(*c).keytable).name_ptr()));
let gap = 2usize;
let inner = (sx.saturating_sub(6) as usize).min(120);
let chip_w = |k: &str, n: &str| k.chars().count() + 1 + n.chars().count();
let mut rows: Vec<Vec<(String, String)>> = Vec::new();
let mut cur: Vec<(String, String)> = Vec::new();
let mut curw = 0usize;
for (k, n) in chips {
let cw = chip_w(&k, &n);
if !cur.is_empty() && curw + gap + cw > inner {
rows.push(std::mem::take(&mut cur));
curw = cw;
} else {
curw += if cur.is_empty() { cw } else { gap + cw };
}
cur.push((k, n));
}
if !cur.is_empty() {
rows.push(cur);
}
rows.truncate(7);
let row_w = |r: &Vec<(String, String)>| -> usize {
r.iter().map(|(k, v)| chip_w(k, v)).sum::<usize>() + gap * r.len().saturating_sub(1)
};
let widest = rows.iter().map(row_w).max().unwrap_or(0) as u16;
let title_w = table.chars().count() as u16 + 4;
let w = (widest + 4).max(title_w).min(sx.saturating_sub(2));
let h = rows.len() as u16 + 2;
let sa = status_at_line(c);
let bottom = if sa > 0 { sa as u16 } else { sy };
let py = bottom.saturating_sub(h);
let px = sx.saturating_sub(w) / 2;
Some(HintLayout {
px,
py,
w,
h,
table,
rows,
})
}
}
unsafe fn hint_check_cb(
_c: *mut client,
data: *mut core::ffi::c_void,
px: u32,
py: u32,
nx: u32,
r: *mut overlay_ranges,
) {
unsafe {
if let Some(l) = data.cast::<HintLayout>().as_ref() {
server_client_overlay_range(
l.px as u32,
l.py as u32,
l.w as u32,
l.h as u32,
px,
py,
nx,
r,
);
} else {
(*r).px[0] = px;
(*r).nx[0] = nx;
(*r).px[1] = 0;
(*r).nx[1] = 0;
(*r).px[2] = 0;
(*r).nx[2] = 0;
}
}
}
unsafe fn hint_draw(c: *mut client, data: *mut core::ffi::c_void, _rctx: *mut screen_redraw_ctx) {
unsafe {
let Some(l) = data.cast::<HintLayout>().as_ref() else {
return;
};
let tty = &raw mut (*c).tty;
let area = Rect::new(0, 0, l.w, l.h);
let mut buf = Buffer::empty(area);
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::Yellow))
.title(Line::from(format!(" {} ", l.table)).centered())
.render(area, &mut buf);
let key_style = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let note_style = Style::default().fg(Color::Gray);
for (ri, row) in l.rows.iter().enumerate() {
let ry = 1 + ri as u16;
let mut cx = 2u16;
for (k, n) in row {
for ch in k.chars() {
if cx >= l.w - 1 {
break;
}
buf[(cx, ry)].set_char(ch).set_style(key_style);
cx += 1;
}
if cx < l.w - 1 {
cx += 1; }
for ch in n.chars() {
if cx >= l.w - 1 {
break;
}
buf[(cx, ry)].set_char(ch).set_style(note_style);
cx += 1;
}
cx += 2; if cx >= l.w - 1 {
break;
}
}
}
blit_tty(tty, &buf, l.px as u32, l.py as u32);
}
}
unsafe fn hint_option_off(_c: *mut client) -> bool {
unsafe {
match global_user_opt("@ztmux-hint") {
Some(v) => !matches!(v.trim(), "on" | "1" | "true" | "yes"),
None => true, }
}
}
pub(crate) unsafe fn reconcile_hint(c: *mut client) {
unsafe {
if !enabled() || c.is_null() || (*c).session.is_null() {
return;
}
if hint_option_off(c) {
if hint_is_ours(c) {
server_client_clear_overlay(c);
}
return;
}
let kt = (*c).keytable;
let want = !kt.is_null() && !server_client_is_default_key_table(c, kt);
if want {
if (*c).overlay_draw.is_none()
&& let Some(layout) = hint_layout(c)
{
let data = Box::into_raw(Box::new(layout)).cast::<core::ffi::c_void>();
server_client_set_overlay(
c,
0,
Some(hint_check_cb),
None,
Some(hint_draw),
None,
Some(hint_free),
None,
data,
);
}
} else if hint_is_ours(c) {
server_client_clear_overlay(c);
}
}
}
unsafe fn pane_marked_for_sync(po: *mut options) -> bool {
unsafe {
if crate::options_::options_get_only_const(po, "@ztmux_sel").is_null() {
return false;
}
crate::cstr_to_str(crate::options_::options_get_string_(po, "@ztmux_sel")) == "1"
}
}
unsafe fn pane_state_colour(wp: *mut window_pane) -> Option<i32> {
unsafe {
let po = (*wp).options;
if crate::options_::options_get_number_(po, "synchronize-panes") != 0 {
Some(196)
} else if pane_marked_for_sync(po) {
Some(214)
} else if (*wp).pipe_fd != -1 {
Some(51)
} else {
None
}
}
}
pub(crate) unsafe fn apply_sync_border(gc: *mut grid_cell, wp: *mut window_pane) {
unsafe {
if !enabled() || wp.is_null() {
return;
}
if let Some(fg) = pane_state_colour(wp) {
(*gc).fg = fg;
(*gc).attr |= grid_attr::GRID_ATTR_BRIGHT;
}
}
}
unsafe fn global_user_opt(name: &str) -> Option<String> {
unsafe {
for oo in [GLOBAL_S_OPTIONS, GLOBAL_OPTIONS, GLOBAL_W_OPTIONS] {
if !oo.is_null() && !crate::options_::options_get_only_const(oo, name).is_null() {
return Some(
crate::cstr_to_str(crate::options_::options_get_string_(oo, name)).to_string(),
);
}
}
None
}
}
unsafe fn global_user_flag(name: &str, default: bool) -> bool {
unsafe {
for oo in [GLOBAL_S_OPTIONS, GLOBAL_OPTIONS, GLOBAL_W_OPTIONS] {
if !oo.is_null() && !crate::options_::options_get_only_const(oo, name).is_null() {
let s = crate::cstr_to_str(crate::options_::options_get_string_(oo, name)).trim();
let off = s.eq_ignore_ascii_case("0")
|| s.eq_ignore_ascii_case("off")
|| s.eq_ignore_ascii_case("false")
|| s.eq_ignore_ascii_case("no");
return !off;
}
}
default
}
}
unsafe fn zellij_mode_on() -> bool {
unsafe {
let v =
global_user_opt("@ztmux-zellij-mode").or_else(|| global_user_opt("@ztmux-pane-names"));
match v {
Some(v) => matches!(v.trim(), "on" | "1" | "true" | "yes"),
None => false, }
}
}
pub(crate) unsafe fn frame_inset() -> u32 {
unsafe { u32::from(enabled() && zellij_mode_on()) }
}
unsafe fn pane_display_name(ctx: *mut screen_redraw_ctx, wp: *mut window_pane) -> String {
unsafe {
let fmt = global_user_opt("@ztmux-pane-name-format")
.filter(|f| !f.trim().is_empty())
.unwrap_or_else(|| "#{pane_index}: #{pane_current_command}".to_string());
if let Ok(cfmt) = std::ffi::CString::new(fmt.trim()) {
let c = (*ctx).c;
let s = (*c).session;
let wl = if s.is_null() { null_mut() } else { (*s).curw };
let ft = format_create_defaults(null_mut(), c, s, wl, wp);
let out = format_expand(ft, cfmt.as_ptr().cast());
let name = crate::cstr_to_str(out).trim().to_string();
crate::free_(out);
format_free(ft);
if !name.is_empty() {
return name;
}
}
let mut idx: u32 = 0;
window_pane_index(wp, &raw mut idx);
format!("pane {idx}")
}
}
unsafe fn scroll_indicator(ctx: *mut screen_redraw_ctx, wp: *mut window_pane) -> String {
unsafe {
let Ok(cfmt) = std::ffi::CString::new("#{scroll_position}\u{1f}#{history_size}") else {
return String::new();
};
let c = (*ctx).c;
let s = (*c).session;
let wl = if s.is_null() { null_mut() } else { (*s).curw };
let ft = format_create_defaults(null_mut(), c, s, wl, wp);
let out = format_expand(ft, cfmt.as_ptr().cast());
let raw = crate::cstr_to_str(out).to_string();
crate::free_(out);
format_free(ft);
let (pos, total) = raw.split_once('\u{1f}').unwrap_or(("", "0"));
let total = total.trim();
if total.is_empty() || total == "0" {
return String::new(); }
let pos = pos.trim();
let pos = if pos.is_empty() { "0" } else { pos };
format!("SCROLL: {pos}/{total}")
}
}
pub(crate) unsafe fn draw_pane_frame(ctx: *mut screen_redraw_ctx, wp: *mut window_pane) {
unsafe {
if !enabled() || wp.is_null() {
return;
}
if (*ctx).c.is_null() || (*(*ctx).c).overlay_draw.is_some() {
return;
}
let inset = frame_inset();
if inset == 0 {
return;
}
let po = (*wp).options;
let state = if crate::options_::options_get_number_(po, "synchronize-panes") != 0 {
Some((Color::Indexed(196), "\u{27f2} SYNCED")) } else if pane_marked_for_sync(po) {
Some((Color::Indexed(214), "\u{25c6} SELECTED")) } else if (*wp).pipe_fd != -1 {
Some((Color::Indexed(51), "\u{26a1} TRIGGER")) } else {
None
};
if (*wp).sy <= 2 {
let width = (*wp).sx as usize;
if width < 4
|| (*wp).yoff < (*ctx).oy
|| (*wp).yoff >= (*ctx).oy + (*ctx).sy
|| (*wp).xoff < (*ctx).ox
|| (*wp).xoff + (*wp).sx > (*ctx).ox + (*ctx).sx
{
return;
}
let color = match state {
Some((col, _)) => col,
None => Color::Indexed(244),
};
let mut row: Vec<char> = vec!['\u{2500}'; width]; row[0] = '\u{256d}'; row[width - 1] = '\u{256e}'; for (i, ch) in format!(" {} ", pane_display_name(ctx, wp))
.chars()
.enumerate()
{
if 2 + i < width - 1 {
row[2 + i] = ch;
}
}
let scroll = scroll_indicator(ctx, wp);
if !scroll.is_empty() {
let label: Vec<char> = format!(" {scroll} ").chars().collect();
let start = width.saturating_sub(1 + label.len());
if start > 3 {
for (i, ch) in label.iter().enumerate() {
row[start + i] = *ch;
}
}
}
let st = Style::default().fg(color);
let area = Rect::new(0, 0, width as u16, 1);
let mut buf = Buffer::empty(area);
for (x, ch) in row.iter().enumerate() {
buf[(x as u16, 0)].set_char(*ch).set_style(st);
}
let yoff = (*wp).yoff - (*ctx).oy
+ if (*ctx).statustop != 0 {
(*ctx).statuslines
} else {
0
};
let xoff = (*wp).xoff - (*ctx).ox;
let tty = &raw mut (*(*ctx).c).tty;
blit_tty(tty, &buf, xoff, yoff);
return;
}
let cell_x = (*wp).xoff.saturating_sub(inset);
let cell_y = (*wp).yoff.saturating_sub(inset);
let cell_sx = ((*wp).sx + 2 * inset) as u16;
let cell_sy = ((*wp).sy + 2 * inset) as u16;
if cell_sx < 4 || cell_sy < 3 {
return;
}
if cell_x < (*ctx).ox
|| cell_y < (*ctx).oy
|| cell_x + cell_sx as u32 > (*ctx).ox + (*ctx).sx
|| cell_y + cell_sy as u32 > (*ctx).oy + (*ctx).sy
{
return;
}
let name = pane_display_name(ctx, wp);
let (color, title, bold) = match state {
Some((col, lbl)) => {
let t = if name.is_empty() {
format!(" {lbl} ")
} else {
format!(" {lbl} \u{b7} {name} ")
};
(col, t, true)
}
None => {
let active = std::ptr::eq(wp, (*(*wp).window).active);
let col = if active {
Color::Green
} else {
Color::Indexed(244)
};
(col, format!(" {name} "), active)
}
};
let c = (*ctx).c;
let tty = &raw mut (*c).tty;
let area = Rect::new(0, 0, cell_sx, cell_sy);
let mut buf = Buffer::empty(area);
let mut style = Style::default().fg(color);
if bold {
style = style.add_modifier(Modifier::BOLD);
}
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(style)
.title(Line::from(title))
.render(area, &mut buf);
let yoff = cell_y - (*ctx).oy
+ if (*ctx).statustop != 0 {
(*ctx).statuslines
} else {
0
};
let xoff = cell_x - (*ctx).ox;
blit_tty_frame(tty, &buf, xoff, yoff);
}
}
unsafe fn blit_tty_frame(tty: *mut tty, buf: &Buffer, px: u32, py: u32) {
unsafe {
let area = buf.area;
for y in 0..area.height {
for x in 0..area.width {
if x != 0 && x != area.width - 1 && y != 0 && y != area.height - 1 {
continue; }
let cell = &buf[(x, y)];
let st = cell.style();
let mut gc = std::mem::MaybeUninit::<grid_cell>::uninit();
memcpy__(gc.as_mut_ptr(), &raw const GRID_DEFAULT_CELL);
let gc = gc.as_mut_ptr();
set_char(gc, cell.symbol());
(*gc).fg = map_color(st.fg);
(*gc).bg = map_color(st.bg);
(*gc).attr = map_modifier(st.add_modifier);
tty_cursor(tty, px + x as u32, py + y as u32);
tty_cell(
tty,
gc,
&raw const GRID_DEFAULT_CELL,
std::ptr::null(),
std::ptr::null_mut(),
);
}
}
}
}
pub(crate) unsafe fn draw_info_bar(ctx: *mut screen_redraw_ctx) {
unsafe {
if frame_inset() == 0 {
return;
}
let c = (*ctx).c;
if c.is_null() || (*c).overlay_draw.is_some() {
return;
}
let width = (*ctx).sx as u16;
if width < 24 || (*ctx).sy < 3 {
return;
}
let tty = &raw mut (*c).tty;
let hints = " \u{2b21} zellij C-s mark \u{b7} M sync \u{b7} : cmd \u{b7} ? keys \u{b7} d detach ";
let area = Rect::new(0, 0, width, 1);
let mut buf = Buffer::empty(area);
let bar = Style::default()
.bg(Color::Indexed(236))
.fg(Color::Indexed(250));
for x in 0..width {
buf[(x, 0)].set_char(' ').set_style(bar);
}
let mut cx = 0u16;
for ch in hints.chars() {
if cx >= width {
break;
}
buf[(cx, 0)].set_char(ch).set_style(bar);
cx += 1;
}
let srow = (*ctx).sy - 1
+ if (*ctx).statustop != 0 {
(*ctx).statuslines
} else {
0
};
blit_tty(tty, &buf, 0, srow);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_mapping_covers_the_palette() {
assert_eq!(map_color(None), 8); assert_eq!(map_color(Some(Color::Reset)), 8);
assert_eq!(map_color(Some(Color::Red)), 1);
assert_eq!(map_color(Some(Color::White)), 97); assert_eq!(map_color(Some(Color::DarkGray)), 90); assert_eq!(map_color(Some(Color::Indexed(5))), 5); assert_eq!(map_color(Some(Color::Indexed(12))), 94); assert_eq!(map_color(Some(Color::Indexed(200))), 200 | 0x0100_0000); assert_eq!(
map_color(Some(Color::Rgb(0x11, 0x22, 0x33))),
crate::colour::colour_join_rgb(0x11, 0x22, 0x33)
);
}
#[test]
fn modifier_mapping() {
assert!(map_modifier(Modifier::REVERSED).contains(grid_attr::GRID_ATTR_REVERSE));
assert!(map_modifier(Modifier::BOLD).contains(grid_attr::GRID_ATTR_BRIGHT));
let both = map_modifier(Modifier::BOLD | Modifier::UNDERLINED);
assert!(both.contains(grid_attr::GRID_ATTR_BRIGHT));
assert!(both.contains(grid_attr::GRID_ATTR_UNDERSCORE));
assert!(map_modifier(Modifier::empty()).is_empty());
}
#[test]
fn strip_markup_removes_style_runs() {
assert_eq!(strip_markup("#[align=centre]Kill"), "Kill");
assert_eq!(strip_markup("Copy #[underscore]word"), "Copy word");
assert_eq!(strip_markup("plain"), "plain");
}
#[test]
fn renders_a_bordered_menu_into_a_buffer() {
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
let items = vec![ListItem::new(Line::from(" Horizontal Split"))];
let block = Block::bordered()
.border_type(BorderType::Rounded)
.title(Line::from(" pane ").centered());
let list = List::new(items)
.block(block)
.highlight_style(Style::default().add_modifier(Modifier::REVERSED));
let mut state = ListState::default();
state.select(Some(0));
StatefulWidget::render(list, area, &mut buf, &mut state);
assert_eq!(buf[(0, 0)].symbol(), "╭");
let row1: String = (1..19).map(|x| buf[(x, 1)].symbol()).collect();
assert!(row1.contains("Horizontal Split"), "got row: {row1:?}");
}
}