Added prompt escape code expansion flag to echo, -p

Added non-formatted runtime to prompt escape codes

Added prompt escape code that expands to the output of a shell function

Reworked internal logic for termios control
This commit is contained in:
2026-01-29 03:46:35 -05:00
parent 4a6a941f1e
commit 222e06bee6
10 changed files with 381 additions and 178 deletions

View File

@@ -1,14 +1,7 @@
use std::sync::LazyLock;
use crate::{
builtin::setup_builtin,
getopt::{get_opts_from_tokens, Opt, OptSet},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state,
builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSet, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state
};
pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
@@ -16,7 +9,7 @@ pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
Opt::Short('n'),
Opt::Short('E'),
Opt::Short('e'),
Opt::Short('r'),
Opt::Short('p'),
]
.into()
});
@@ -26,6 +19,7 @@ bitflags! {
const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010;
const USE_ESCAPE = 0b000100;
const USE_PROMPT = 0b001000;
}
}
@@ -49,11 +43,13 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
borrow_fd(STDOUT_FILENO)
};
let mut echo_output = argv
let mut echo_output = prepare_echo_args(argv
.into_iter()
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
.collect::<Vec<_>>()
.join(" ");
.collect::<Vec<_>>(),
flags.contains(EchoFlags::USE_ESCAPE),
flags.contains(EchoFlags::USE_PROMPT)
)?.join(" ");
if !flags.contains(EchoFlags::NO_NEWLINE) {
echo_output.push('\n')
@@ -66,6 +62,122 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(())
}
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> {
if !use_escape {
if use_prompt {
let expanded: ShResult<Vec<String>> = argv
.into_iter()
.map(|s| expand_prompt(s.as_str()))
.collect();
return expanded
}
return Ok(argv);
}
let mut prepared_args = Vec::with_capacity(argv.len());
for arg in argv {
let mut prepared_arg = String::new();
if use_prompt {
prepared_arg = expand_prompt(&prepared_arg)?;
}
let mut chars = arg.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next_char) = chars.peek() {
match next_char {
'n' => {
prepared_arg.push('\n');
chars.next();
}
't' => {
prepared_arg.push('\t');
chars.next();
}
'r' => {
prepared_arg.push('\r');
chars.next();
}
'a' => {
prepared_arg.push('\x07');
chars.next();
}
'b' => {
prepared_arg.push('\x08');
chars.next();
}
'e' | 'E' => {
prepared_arg.push('\x1b');
chars.next();
}
'x' => {
chars.next(); // consume 'x'
let mut hex_digits = String::new();
for _ in 0..2 {
if let Some(&hex_char) = chars.peek() {
if hex_char.is_ascii_hexdigit() {
hex_digits.push(hex_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('x');
prepared_arg.push_str(&hex_digits);
}
}
'0' => {
chars.next(); // consume '0'
let mut octal_digits = String::new();
for _ in 0..3 {
if let Some(&octal_char) = chars.peek() {
if ('0'..='7').contains(&octal_char) {
octal_digits.push(octal_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('0');
prepared_arg.push_str(&octal_digits);
}
}
'\\' => {
prepared_arg.push('\\');
chars.next();
}
_ => prepared_arg.push(c),
}
} else {
prepared_arg.push(c);
}
} else {
prepared_arg.push(c);
}
}
prepared_args.push(prepared_arg);
}
Ok(prepared_args)
}
pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
let mut flags = EchoFlags::empty();
@@ -82,6 +194,7 @@ pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
'n' => flags |= EchoFlags::NO_NEWLINE,
'r' => flags |= EchoFlags::USE_STDERR,
'e' => flags |= EchoFlags::USE_ESCAPE,
'p' => flags |= EchoFlags::USE_PROMPT,
_ => unreachable!(),
}
}

View File

@@ -11,7 +11,7 @@ use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFl
use crate::parse::{Redir, RedirType};
use crate::prelude::*;
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::state::{LogTab, VarFlags, read_jobs, read_vars, write_jobs, write_meta, write_vars};
use crate::state::{LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars};
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
@@ -789,13 +789,15 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
io_stack.push_frame(cmd_sub_io_frame);
if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) {
eprintln!("{e}");
exit(1);
unsafe { libc::_exit(1) };
}
exit(0);
unsafe { libc::_exit(0) };
}
ForkResult::Parent { child } => {
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
let status = waitpid(child, Some(WtFlag::WSTOPPED))?;
// Reclaim terminal foreground in case child changed it
crate::jobs::take_term()?;
match status {
WtStat::Exited(_, _) => {
flog!(DEBUG, "filling buffer");
@@ -1423,9 +1425,11 @@ pub enum PromptTk {
AsciiOct(i32),
Text(String),
AnsiSeq(String),
Function(String), // Expands to the output of any defined shell function
VisGrp,
UserSeq,
Runtime,
RuntimeMillis,
RuntimeFormatted,
Weekday,
Dquote,
Squote,
@@ -1442,6 +1446,8 @@ pub enum PromptTk {
SuccessSymbol,
FailureSymbol,
JobCount,
VisGroupOpen,
VisGroupClose,
}
pub fn format_cmd_runtime(dur: std::time::Duration) -> String {
@@ -1646,10 +1652,46 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
'$' => tokens.push(PromptTk::PromptSymbol),
'n' => tokens.push(PromptTk::Text("\n".into())),
'r' => tokens.push(PromptTk::Text("\r".into())),
'T' => tokens.push(PromptTk::Runtime),
't' => tokens.push(PromptTk::RuntimeMillis),
'T' => tokens.push(PromptTk::RuntimeFormatted),
'\\' => tokens.push(PromptTk::Text("\\".into())),
'"' => tokens.push(PromptTk::Text("\"".into())),
'\'' => tokens.push(PromptTk::Text("'".into())),
'(' => tokens.push(PromptTk::VisGroupOpen),
')' => tokens.push(PromptTk::VisGroupClose),
'!' => {
let mut func_name = String::new();
let is_braced = chars.peek() == Some(&'{');
while let Some(ch) = chars.peek() {
match ch {
'}' if is_braced => {
chars.next();
break;
}
'A'..='Z' | 'a'..='z' | '0'..='9' | '_' => {
func_name.push(*ch);
chars.next();
}
_ => {
if is_braced {
// Invalid character in braced function name
tokens.push(PromptTk::Text(format!("\\!{{{func_name}")));
break;
} else {
// End of unbraced function name
let func_exists = read_logic(|l| l.get_func(&func_name).is_some());
if func_exists {
tokens.push(PromptTk::Function(func_name));
} else {
tokens.push(PromptTk::Text(format!("\\!{func_name}")));
}
break;
}
}
}
}
}
'e' => {
if chars.next() == Some('[') {
let mut params = String::new();
@@ -1733,65 +1775,84 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
let mut result = String::new();
while let Some(token) = tokens.next() {
match token {
PromptTk::AsciiOct(_) => todo!(),
PromptTk::Text(txt) => result.push_str(&txt),
PromptTk::AnsiSeq(params) => result.push_str(&params),
PromptTk::Runtime => {
if let Some(runtime) = write_meta(|m| m.stop_timer()) {
let runtime_fmt = format_cmd_runtime(runtime);
result.push_str(&runtime_fmt);
}
}
PromptTk::Pwd => {
let mut pwd = std::env::var("PWD").unwrap();
let home = std::env::var("HOME").unwrap();
if pwd.starts_with(&home) {
pwd = pwd.replacen(&home, "~", 1);
}
result.push_str(&pwd);
}
PromptTk::PwdShort => {
let mut path = std::env::var("PWD").unwrap();
let home = std::env::var("HOME").unwrap();
if path.starts_with(&home) {
path = path.replacen(&home, "~", 1);
}
let pathbuf = PathBuf::from(&path);
let mut segments = pathbuf.iter().count();
let mut path_iter = pathbuf.iter();
while segments > 4 {
path_iter.next();
segments -= 1;
}
let path_rebuilt: PathBuf = path_iter.collect();
let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string();
if path_rebuilt.starts_with(&home) {
path_rebuilt = path_rebuilt.replacen(&home, "~", 1);
}
result.push_str(&path_rebuilt);
}
PromptTk::Hostname => {
let hostname = std::env::var("HOST").unwrap();
result.push_str(&hostname);
}
PromptTk::HostnameShort => todo!(),
PromptTk::ShellName => result.push_str("fern"),
PromptTk::Username => {
let username = std::env::var("USER").unwrap();
result.push_str(&username);
}
PromptTk::PromptSymbol => {
let uid = std::env::var("UID").unwrap();
let symbol = if &uid == "0" { '#' } else { '$' };
result.push(symbol);
}
PromptTk::ExitCode => todo!(),
PromptTk::SuccessSymbol => todo!(),
PromptTk::FailureSymbol => todo!(),
PromptTk::JobCount => todo!(),
_ => unimplemented!(),
}
match token {
PromptTk::AsciiOct(_) => todo!(),
PromptTk::Text(txt) => result.push_str(&txt),
PromptTk::AnsiSeq(params) => result.push_str(&params),
PromptTk::RuntimeMillis => {
if let Some(runtime) = write_meta(|m| m.stop_timer()) {
let runtime_millis = runtime.as_millis().to_string();
result.push_str(&runtime_millis);
}
}
PromptTk::RuntimeFormatted => {
if let Some(runtime) = write_meta(|m| m.stop_timer()) {
let runtime_fmt = format_cmd_runtime(runtime);
result.push_str(&runtime_fmt);
}
}
PromptTk::Pwd => {
let mut pwd = std::env::var("PWD").unwrap();
let home = std::env::var("HOME").unwrap();
if pwd.starts_with(&home) {
pwd = pwd.replacen(&home, "~", 1);
}
result.push_str(&pwd);
}
PromptTk::PwdShort => {
let mut path = std::env::var("PWD").unwrap();
let home = std::env::var("HOME").unwrap();
if path.starts_with(&home) {
path = path.replacen(&home, "~", 1);
}
let pathbuf = PathBuf::from(&path);
let mut segments = pathbuf.iter().count();
let mut path_iter = pathbuf.iter();
while segments > 4 {
path_iter.next();
segments -= 1;
}
let path_rebuilt: PathBuf = path_iter.collect();
let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string();
if path_rebuilt.starts_with(&home) {
path_rebuilt = path_rebuilt.replacen(&home, "~", 1);
}
result.push_str(&path_rebuilt);
}
PromptTk::Hostname => {
let hostname = std::env::var("HOST").unwrap();
result.push_str(&hostname);
}
PromptTk::HostnameShort => todo!(),
PromptTk::ShellName => result.push_str("fern"),
PromptTk::Username => {
let username = std::env::var("USER").unwrap();
result.push_str(&username);
}
PromptTk::PromptSymbol => {
let uid = std::env::var("UID").unwrap();
let symbol = if &uid == "0" { '#' } else { '$' };
result.push(symbol);
}
PromptTk::ExitCode => todo!(),
PromptTk::SuccessSymbol => todo!(),
PromptTk::FailureSymbol => todo!(),
PromptTk::JobCount => todo!(),
PromptTk::Function(f) => {
flog!(DEBUG, "Expanding prompt function: {}", f);
let output = expand_cmd_sub(&f)?;
result.push_str(&output);
}
PromptTk::VisGrp => todo!(),
PromptTk::UserSeq => todo!(),
PromptTk::Weekday => todo!(),
PromptTk::Dquote => todo!(),
PromptTk::Squote => todo!(),
PromptTk::Return => todo!(),
PromptTk::Newline => todo!(),
PromptTk::VisGroupOpen => todo!(),
PromptTk::VisGroupClose => todo!(),
}
}
Ok(result)

View File

@@ -1,6 +1,6 @@
use termios::{LocalFlags, Termios};
use crate::{prelude::*, state::write_jobs};
use crate::{prelude::*};
///
/// The previous state of the terminal options.
///
@@ -31,64 +31,46 @@ use crate::{prelude::*, state::write_jobs};
/// lifecycle could lead to undefined behavior.
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() {
unsafe {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
Some(termios)
} else {
None
});
}
}
#[allow(static_mut_refs)]
///Access the saved termios
///
///# Safety
///This function is unsafe because it accesses a public mutable static value.
/// This function should only ever be called after save_termios() has already
/// been called.
pub unsafe fn get_saved_termios() -> Option<Termios> { unsafe {
// SAVED_TERMIOS should *only ever* be set once and accessed once
// Set at the start of the program, and accessed during the exit of the program
// to reset the termios. Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten()
}}
/// Set termios to not echo control characters, like ^Z for instance
pub fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
}
#[derive(Debug)]
pub struct TermiosGuard {
saved_termios: Option<Termios>
}
pub fn sh_quit(code: i32) -> ! {
write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok();
}
});
if let Some(termios) = unsafe { get_saved_termios() } {
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
}
if code == 0 {
eprintln!("exit");
} else {
eprintln!("exit {code}");
}
exit(code);
impl TermiosGuard {
pub fn new(new_termios: Termios) -> Self {
let mut new = Self { saved_termios: None };
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
new.saved_termios = Some(current_termios);
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&new_termios,
).unwrap();
}
new
}
}
impl Default for TermiosGuard {
fn default() -> Self {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
Self::new(termios)
}
}
impl Drop for TermiosGuard {
fn drop(&mut self) {
if let Some(saved) = &self.saved_termios {
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
saved,
).unwrap();
}
}
}

View File

@@ -18,11 +18,14 @@ pub mod state;
#[cfg(test)]
pub mod tests;
use std::process::ExitCode;
use std::sync::atomic::Ordering;
use crate::libsh::error::ShErrKind;
use crate::libsh::sys::{save_termios, set_termios};
use crate::libsh::sys::TermiosGuard;
use crate::parse::execute::exec_input;
use crate::prelude::*;
use crate::signal::{check_signals, sig_setup, signals_pending};
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::source_rc;
use clap::Parser;
use shopt::FernEditMode;
@@ -53,12 +56,12 @@ fn kickstart_lazy_evals() {
read_vars(|_| {});
}
fn main() {
fn main() -> ExitCode {
kickstart_lazy_evals();
let args = FernArgs::parse();
if args.version {
println!("fern {}", env!("CARGO_PKG_VERSION"));
return;
return ExitCode::SUCCESS;
}
if let Some(path) = args.script {
@@ -66,17 +69,21 @@ fn main() {
} else {
fern_interactive();
}
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
}
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
let path = path.as_ref();
if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display());
exit(1);
QUIT_CODE.store(1, Ordering::SeqCst);
return;
}
let Ok(input) = fs::read_to_string(path) else {
eprintln!("fern: Failed to read input file: {}", path.display());
exit(1);
QUIT_CODE.store(1, Ordering::SeqCst);
return;
};
write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string()));
@@ -86,13 +93,19 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
if let Err(e) = exec_input(input, None) {
eprintln!("{e}");
exit(1);
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
}
_ => {
QUIT_CODE.store(1, Ordering::SeqCst);
}
}
}
}
fn fern_interactive() {
save_termios();
set_termios();
let _termios_guard = TermiosGuard::default(); // sets raw mode, restores termios on drop
sig_setup();
if let Err(e) = source_rc() {
@@ -129,37 +142,52 @@ fn fern_interactive() {
line
}
Err(e) => {
if let ShErrKind::ReadlineIntr(partial) = e.kind() {
// Did we get signaled? Check signal flags
// If nothing to worry about, retry the readline
while signals_pending() {
if let Err(e) = check_signals() {
if let ShErrKind::ClearReadline = e.kind() {
partial_input.clear();
if !signals_pending() {
continue 'outer;
}
};
eprintln!("{e}");
match e.kind() {
ShErrKind::ReadlineIntr(partial) => {
// Did we get signaled? Check signal flags
// If nothing to worry about, retry the readline with the unfinished input
while signals_pending() {
if let Err(e) = check_signals() {
if let ShErrKind::ClearReadline = e.kind() {
partial_input.clear();
if !signals_pending() {
continue 'outer;
}
};
eprintln!("{e}");
}
}
}
partial_input = partial.to_string();
continue;
} else {
eprintln!("{e}");
readline_err_count += 1;
if readline_err_count == 20 {
eprintln!("reached maximum readline error count, exiting");
break;
} else {
partial_input = partial.to_string();
continue;
}
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return;
}
_ => {
eprintln!("{e}");
readline_err_count += 1;
if readline_err_count == 20 {
eprintln!("reached maximum readline error count, exiting");
break;
} else {
continue;
}
}
}
}
};
if let Err(e) = exec_input(input, None) {
eprintln!("{e}");
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return;
}
_ => {
eprintln!("{e}");
}
}
}
}
}

View File

@@ -1282,11 +1282,30 @@ impl ParseStream {
assignments.push(assign)
} else if is_keyword {
return Ok(None);
} else if prefix_tk.class == TkRule::Sep {
// Separator ends the prefix section - add it so commit() consumes it
node_tks.push(prefix_tk.clone());
break;
} else {
// Other non-prefix token ends the prefix section
break;
}
}
if argv.is_empty() && assignments.is_empty() {
return Ok(None);
if argv.is_empty() {
if assignments.is_empty() {
return Ok(None);
} else {
// If we have assignments but no command word,
// return the assignment-only command without parsing more tokens
self.commit(node_tks.len());
return Ok(Some(Node {
class: NdRule::Command { assignments, argv },
tokens: node_tks,
flags: NdFlags::empty(),
redirs,
}));
}
}
while let Some(tk) = tk_iter.next() {

View File

@@ -11,17 +11,19 @@ use crate::{
/// Initialize the line editor
fn get_prompt() -> ShResult<String> {
let Ok(prompt) = env::var("PS1") else {
// prompt expands to:
// default prompt expands to:
//
// username@hostname
// short/path/to/pwd/
// $ _
let default =
"\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
return expand_prompt(default);
};
let sanitized = format!("\\e[0m{prompt}");
flog!(DEBUG, "Using prompt: {}", sanitized.replace("\n", "\\n"));
expand_prompt(&prompt)
expand_prompt(&sanitized)
}
pub fn readline(edit_mode: FernEditMode, initial: Option<&str>) -> ShResult<String> {

View File

@@ -8,7 +8,6 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis
use crate::libsh::{
error::{ShErr, ShErrKind, ShResult},
sys::sh_quit,
term::{Style, Styled},
};
use crate::prelude::*;
@@ -95,7 +94,7 @@ impl Readline for FernVi {
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() {
std::mem::drop(raw_mode_guard);
sh_quit(0);
return Err(ShErr::simple(ShErrKind::CleanExit(0), "exit"));
} else {
self.editor.buffer.clear();
continue;

View File

@@ -687,7 +687,7 @@ impl LineWriter for TermWriter {
for _ in 0..rows_to_clear {
self.buffer.push_str("\x1b[2K\x1b[A");
}
self.buffer.push_str("\x1b[2K");
self.buffer.push_str("\x1b[2K\r"); // Clear line and return to column 0
write_all(self.out, self.buffer.as_str())?;
self.buffer.clear();
Ok(())

View File

@@ -15,8 +15,8 @@ static GOT_SIGTSTP: AtomicBool = AtomicBool::new(false);
static GOT_SIGCHLD: AtomicBool = AtomicBool::new(false);
static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
pub fn signals_pending() -> bool {
GOT_SIGINT.load(Ordering::SeqCst)

View File

@@ -686,7 +686,6 @@ impl MetaTab {
pub fn stop_timer(&mut self) -> Option<Duration> {
self
.runtime_start
.take() // runtime_start returns to None
.map(|start| start.elapsed()) // return the duration, if any
}
}