termsize: better types

The u16 is implied by libc::winsize.
This commit is contained in:
Johannes Altmanninger
2025-11-07 16:08:44 +01:00
parent 382027663f
commit 336be1e36d
6 changed files with 110 additions and 94 deletions

View File

@@ -1339,7 +1339,7 @@ macro_rules! write_to_output {
pub fn reformat_for_screen(msg: &wstr, termsize: &Termsize) -> WString {
let mut buff = WString::new();
let screen_width = termsize.width;
let screen_width = isize::try_from(termsize.width()).unwrap();
if screen_width != 0 {
let mut start = 0;
let mut pos = start;

View File

@@ -747,10 +747,14 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool
// Initialize termsize variables.
let termsize = termsize::SHARED_CONTAINER.initialize(vars as &dyn Environment);
if vars.get_unless_empty(L!("COLUMNS")).is_none() {
vars.set_one(L!("COLUMNS"), EnvMode::GLOBAL, termsize.width.to_wstring());
vars.set_one(
L!("COLUMNS"),
EnvMode::GLOBAL,
termsize.width().to_wstring(),
);
}
if vars.get_unless_empty(L!("LINES")).is_none() {
vars.set_one(L!("LINES"), EnvMode::GLOBAL, termsize.height.to_wstring());
vars.set_one(L!("LINES"), EnvMode::GLOBAL, termsize.height().to_wstring());
}
// Set fish_bind_mode to "default".

View File

@@ -650,8 +650,8 @@ pub fn set_prefix(&mut self, pref: &wstr, highlight: bool /* = true */) {
// Sets the terminal size.
pub fn set_term_size(&mut self, ts: &Termsize) {
self.available_term_width = usize::try_from(ts.width).unwrap_or_default();
self.available_term_height = usize::try_from(ts.height).unwrap_or_default();
self.available_term_width = ts.width();
self.available_term_height = ts.height();
}
// Changes the selected completion in the given direction according to the layout of the given
@@ -1270,6 +1270,7 @@ mod tests {
use crate::tests::prelude::*;
use crate::wchar::prelude::*;
use crate::wcstringutil::StringFuzzyMatch;
use std::num::NonZeroU16;
#[test]
#[serial]
@@ -1368,8 +1369,11 @@ fn test_pager_layout() {
// These tests are woefully incomplete
// They only test the truncation logic for a single completion
let rendered_line = |pager: &mut Pager, width: isize| {
pager.set_term_size(&Termsize::new(width, 24));
let rendered_line = |pager: &mut Pager, width: u16| {
pager.set_term_size(&Termsize::new(
NonZeroU16::new(width).unwrap(),
Termsize::DEFAULT_HEIGHT,
));
let rendering = pager.render();
let sd = &rendering.screen_data;
assert_eq!(sd.line_count(), 1);

View File

@@ -391,7 +391,7 @@ pub fn reader_pop() {
if let Some(new_reader) = current_data() {
new_reader
.screen
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap());
.reset_abandoning_line(termsize_last().width());
} else {
Outputter::stdoutput().borrow_mut().reset_text_face();
*commandline_state_snapshot() = CommandlineState::new();
@@ -2320,8 +2320,7 @@ fn readline(
//
// I can't see a good way around this.
if !self.first_prompt {
self.screen
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap());
self.screen.reset_abandoning_line(termsize_last().width());
}
self.first_prompt = false;
@@ -2810,8 +2809,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
Edit::new(0..self.command_line_len(), L!("").to_owned()),
);
if c == rl::CancelCommandline {
self.screen
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap());
self.screen.reset_abandoning_line(termsize_last().width());
}
// Post fish_cancel.
@@ -3092,8 +3090,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) {
L!("fish_posterror").to_owned(),
vec![self.command_line.text().to_owned()],
);
self.screen
.reset_abandoning_line(usize::try_from(termsize_last().width).unwrap());
self.screen.reset_abandoning_line(termsize_last().width());
}
}
rl::HistoryPrefixSearchBackward
@@ -5358,7 +5355,7 @@ fn history_pager_search(
// We can still push fish further upward in case the first entry is multiline,
// but that can't really be helped.
// (subtract 2 for the search line and the prompt)
let page_size = usize::try_from(cmp::max(termsize_last().height / 2 - 2, 12)).unwrap();
let page_size = cmp::max(termsize_last().height() / 2 - 2, 12);
let mut completions = Vec::with_capacity(page_size);
let mut search = HistorySearch::new_with(
history.clone(),

View File

@@ -14,6 +14,7 @@
use std::cell::RefCell;
use std::collections::LinkedList;
use std::io::Write;
use std::num::NonZeroU16;
use std::ops::Range;
use std::sync::Mutex;
use std::sync::atomic::AtomicU32;
@@ -294,8 +295,8 @@ pub fn write(
is_final_rendering: bool,
) {
let curr_termsize = termsize_last();
let screen_width = curr_termsize.width;
let screen_height = curr_termsize.height;
let screen_width = curr_termsize.width();
let screen_height = curr_termsize.height();
static REPAINTS: AtomicU32 = AtomicU32::new(0);
FLOGF!(
screen,
@@ -333,8 +334,6 @@ struct ScrolledCursor {
if screen_width < 4 || screen_height == 0 {
return;
}
let screen_width = usize::try_from(screen_width).unwrap();
let screen_height = usize::try_from(screen_height).unwrap();
// Compute a layout.
let layout = compute_layout(
@@ -474,18 +473,20 @@ struct ScrolledCursor {
} else {
0
};
let pager_available_height = std::cmp::max(
1,
curr_termsize
.height
.saturating_sub_unsigned(full_line_count),
);
fn saturating_sub(m: NonZeroU16, s: usize) -> NonZeroU16 {
NonZeroU16::new(std::cmp::max(
1,
m.get().saturating_sub(s.try_into().unwrap_or(u16::MAX)),
))
.unwrap()
}
let pager_available_height = saturating_sub(curr_termsize.height_u16(), full_line_count);
// Now that we've output everything, set the cursor to the position that we saved in the loop
// above.
self.desired.cursor = match pager_search_field_position {
Some(pager_cursor_pos)
if pager_available_height >= isize::try_from(PAGER_MIN_HEIGHT).unwrap() =>
if usize::from(pager_available_height.get()) >= PAGER_MIN_HEIGHT =>
{
Cursor {
x: pager_cursor_pos,
@@ -508,7 +509,7 @@ struct ScrolledCursor {
// Re-render our completions page if necessary. Limit the term size of the pager to the true
// term size, minus the number of lines consumed by our string.
pager.set_term_size(&Termsize::new(
std::cmp::max(1, curr_termsize.width),
curr_termsize.width_u16(),
pager_available_height,
));

View File

@@ -6,77 +6,81 @@
use crate::wchar::prelude::*;
use crate::wutil::fish_wcstoi;
use std::mem::MaybeUninit;
use std::num::NonZeroU16;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Termsize {
/// Width of the terminal, in columns.
// TODO: Change to u32
pub width: isize,
width: NonZeroU16,
/// Height of the terminal, in rows.
// TODO: Change to u32
pub height: isize,
height: NonZeroU16,
}
// A counter which is incremented every SIGWINCH, or when the tty is otherwise invalidated.
static TTY_TERMSIZE_GEN_COUNT: AtomicU32 = AtomicU32::new(0);
/// Convert an environment variable to an int, or return a default value.
/// Convert an environment variable to an int.
/// The int must be >0 and <USHRT_MAX (from struct winsize).
fn var_to_int_or(var: Option<EnvVar>, default: isize) -> isize {
let val: WString = var.map(|v| v.as_string()).unwrap_or_default();
if !val.is_empty() {
if let Ok(proposed) = fish_wcstoi(&val) {
if proposed > 0 && proposed <= u16::MAX as i32 {
return proposed as isize;
}
}
}
default
fn var_to_int(var: Option<EnvVar>) -> Option<NonZeroU16> {
var.and_then(|v| fish_wcstoi(&v.as_string()).ok())
.and_then(|i| u16::try_from(i).ok())
.and_then(NonZeroU16::new)
}
/// Return a termsize from ioctl, or None on error or if not supported.
fn read_termsize_from_tty() -> Option<Termsize> {
let mut ret: Option<Termsize> = None;
// Note: historically we've supported libc::winsize not existing.
let mut winsize = MaybeUninit::<libc::winsize>::uninit();
if unsafe { libc::ioctl(0, libc::TIOCGWINSZ, winsize.as_mut_ptr()) } >= 0 {
let mut winsize = unsafe { winsize.assume_init() };
// 0 values are unusable, fall back to the default instead.
if winsize.ws_col == 0 {
FLOG!(
term_support,
L!("Terminal has 0 columns, falling back to default width")
);
winsize.ws_col = Termsize::DEFAULT_WIDTH as u16;
let winsize = {
let mut winsize = MaybeUninit::<libc::winsize>::uninit();
if unsafe { libc::ioctl(0, libc::TIOCGWINSZ, winsize.as_mut_ptr()) } < 0 {
return None;
}
if winsize.ws_row == 0 {
FLOG!(
term_support,
L!("Terminal has 0 rows, falling back to default height")
);
winsize.ws_row = Termsize::DEFAULT_HEIGHT as u16;
}
ret = Some(Termsize::new(
winsize.ws_col as isize,
winsize.ws_row as isize,
));
}
ret
unsafe { winsize.assume_init() }
};
let width = NonZeroU16::new(winsize.ws_col).unwrap_or_else(|| {
FLOG!(
term_support,
L!("Terminal has 0 columns, falling back to default width")
);
Termsize::DEFAULT_WIDTH
});
let height = NonZeroU16::new(winsize.ws_row).unwrap_or_else(|| {
FLOG!(
term_support,
L!("Terminal has 0 rows, falling back to default height")
);
Termsize::DEFAULT_HEIGHT
});
Some(Termsize::new(width, height))
}
impl Termsize {
/// Default width and height.
pub const DEFAULT_WIDTH: isize = 80;
pub const DEFAULT_HEIGHT: isize = 24;
pub const DEFAULT_WIDTH: NonZeroU16 = NonZeroU16::new(80).unwrap();
pub const DEFAULT_HEIGHT: NonZeroU16 = NonZeroU16::new(24).unwrap();
/// Construct from width and height.
pub fn new(width: isize, height: isize) -> Self {
pub fn new(width: NonZeroU16, height: NonZeroU16) -> Self {
Self { width, height }
}
pub fn width_u16(&self) -> NonZeroU16 {
self.width
}
pub fn height_u16(&self) -> NonZeroU16 {
self.height
}
pub fn width(&self) -> usize {
usize::from(self.width.get())
}
pub fn height(&self) -> usize {
usize::from(self.height.get())
}
/// Return a default-sized termsize.
pub fn defaults() -> Self {
Self::new(Self::DEFAULT_WIDTH, Self::DEFAULT_HEIGHT)
@@ -147,14 +151,11 @@ pub fn last(&self) -> Termsize {
/// This will prefer to use COLUMNS and LINES, but will fall back to the tty size reader.
/// This does not change any variables in the environment.
pub fn initialize(&self, vars: &dyn Environment) -> Termsize {
let new_termsize = Termsize {
width: var_to_int_or(vars.getf(L!("COLUMNS"), EnvMode::GLOBAL), -1),
height: var_to_int_or(vars.getf(L!("LINES"), EnvMode::GLOBAL), -1),
};
let width = var_to_int(vars.getf(L!("COLUMNS"), EnvMode::GLOBAL));
let height = var_to_int(vars.getf(L!("LINES"), EnvMode::GLOBAL));
let mut data = self.data.lock().unwrap();
if new_termsize.width > 0 && new_termsize.height > 0 {
data.mark_override_from_env(new_termsize);
if let (Some(width), Some(height)) = (width, height) {
data.mark_override_from_env(Termsize { width, height });
} else {
data.last_tty_gen_count = TTY_TERMSIZE_GEN_COUNT.load(Ordering::Relaxed);
data.last_from_tty = (self.tty_size_reader)();
@@ -197,8 +198,16 @@ fn updating(&self, parser: &Parser) -> Termsize {
fn set_columns_lines_vars(&self, val: Termsize, parser: &Parser) {
let saved = self.setting_env_vars.swap(true, Ordering::Relaxed);
parser.set_var_and_fire(L!("COLUMNS"), EnvMode::GLOBAL, vec![val.width.to_wstring()]);
parser.set_var_and_fire(L!("LINES"), EnvMode::GLOBAL, vec![val.height.to_wstring()]);
parser.set_var_and_fire(
L!("COLUMNS"),
EnvMode::GLOBAL,
vec![val.width().to_wstring()],
);
parser.set_var_and_fire(
L!("LINES"),
EnvMode::GLOBAL,
vec![val.height().to_wstring()],
);
self.setting_env_vars.store(saved, Ordering::Relaxed);
}
@@ -210,15 +219,9 @@ pub(crate) fn handle_columns_lines_var_change(&self, vars: &dyn Environment) {
}
// Construct a new termsize from COLUMNS and LINES, then set it in our data.
let new_termsize = Termsize {
width: vars
.getf(L!("COLUMNS"), EnvMode::GLOBAL)
.map(|v| v.as_string())
.and_then(|v| fish_wcstoi(&v).ok().map(|h| h as isize))
width: var_to_int(vars.getf(L!("COLUMNS"), EnvMode::GLOBAL))
.unwrap_or(Termsize::DEFAULT_WIDTH),
height: vars
.getf(L!("LINES"), EnvMode::GLOBAL)
.map(|v| v.as_string())
.and_then(|v| fish_wcstoi(&v).ok().map(|h| h as isize))
height: var_to_int(vars.getf(L!("LINES"), EnvMode::GLOBAL))
.unwrap_or(Termsize::DEFAULT_HEIGHT),
};
@@ -289,8 +292,8 @@ fn stubby_termsize() -> Option<Termsize> {
// Haha we change the value, it doesn't even know.
*STUBBY_TERMSIZE.lock().unwrap() = Some(Termsize {
width: 42,
height: 84,
width: NonZeroU16::new(42).unwrap(),
height: NonZeroU16::new(84).unwrap(),
});
assert_eq!(ts.last(), Termsize::defaults());
@@ -299,9 +302,16 @@ fn stubby_termsize() -> Option<Termsize> {
handle_winch();
assert_eq!(ts.last(), Termsize::defaults());
let new_test_termsize = |width, height| {
Termsize::new(
NonZeroU16::new(width).unwrap(),
NonZeroU16::new(height).unwrap(),
)
};
// Ok now we tell it to update.
ts.updating(&parser);
assert_eq!(ts.last(), Termsize::new(42, 84));
assert_eq!(ts.last(), new_test_termsize(42, 84));
assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "42");
assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "84");
@@ -310,17 +320,17 @@ fn stubby_termsize() -> Option<Termsize> {
vars.set_one(L!("COLUMNS"), env_global, L!("75").to_owned());
vars.set_one(L!("LINES"), env_global, L!("150").to_owned());
ts.handle_columns_lines_var_change(parser.vars());
assert_eq!(ts.last(), Termsize::new(75, 150));
assert_eq!(ts.last(), new_test_termsize(75, 150));
assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "75");
assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "150");
vars.set_one(L!("COLUMNS"), env_global, L!("33").to_owned());
ts.handle_columns_lines_var_change(parser.vars());
assert_eq!(ts.last(), Termsize::new(33, 150));
assert_eq!(ts.last(), new_test_termsize(33, 150));
// Oh it got SIGWINCH, now the tty matters again.
handle_winch();
assert_eq!(ts.last(), Termsize::new(33, 150));
assert_eq!(ts.last(), new_test_termsize(33, 150));
assert_eq!(ts.updating(&parser), stubby_termsize().unwrap());
assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "42");
assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "84");
@@ -329,7 +339,7 @@ fn stubby_termsize() -> Option<Termsize> {
vars.set_one(L!("COLUMNS"), env_global, L!("83").to_owned());
vars.set_one(L!("LINES"), env_global, L!("38").to_owned());
ts.initialize(vars);
assert_eq!(ts.last(), Termsize::new(83, 38));
assert_eq!(ts.last(), new_test_termsize(83, 38));
// initialize() even beats the tty reader until a sigwinch.
let ts2 = TermsizeContainer {
@@ -339,7 +349,7 @@ fn stubby_termsize() -> Option<Termsize> {
};
ts.initialize(parser.vars());
ts2.updating(&parser);
assert_eq!(ts.last(), Termsize::new(83, 38));
assert_eq!(ts.last(), new_test_termsize(83, 38));
handle_winch();
assert_eq!(ts2.updating(&parser), stubby_termsize().unwrap());
}