diff --git a/crates/gettext-extraction/src/lib.rs b/crates/gettext-extraction/src/lib.rs index 5610d4d6a..2ecc9f954 100644 --- a/crates/gettext-extraction/src/lib.rs +++ b/crates/gettext-extraction/src/lib.rs @@ -2,13 +2,53 @@ use proc_macro::TokenStream; use std::{ffi::OsString, fs::OpenOptions, io::Write}; +fn unescape_multiline_rust_string(s: String) -> String { + if !s.contains('\n') { + return s; + } + let mut unescaped = String::new(); + enum State { + Ground, + Escaped, + ContinuationLineLeadingWhitespace, + } + use State::*; + let mut state = Ground; + for c in s.chars() { + match state { + Ground => match c { + '\\' => state = Escaped, + _ => { + unescaped.push(c); + } + }, + Escaped => match c { + '\\' => { + unescaped.push('\\'); + state = Ground + } + '\n' => state = ContinuationLineLeadingWhitespace, + _ => panic!("Unsupported escape sequence '\\{c}' in message string '{s}'"), + }, + ContinuationLineLeadingWhitespace => match c { + ' ' | '\t' => (), + _ => { + unescaped.push(c); + state = Ground + } + }, + } + } + unescaped +} + fn append_po_entry_to_file(message: &TokenStream, file_name: &OsString) { let mut file = OpenOptions::new() .create(true) .append(true) .open(file_name) .unwrap_or_else(|e| panic!("Could not open file {file_name:?}: {e}")); - let message_string = message.to_string(); + let message_string = unescape_multiline_rust_string(message.to_string()); if message_string.contains('\n') { panic!("Gettext strings may not contain unescaped newlines. Unescaped newline found in '{message_string}'") } diff --git a/po/de.po b/po/de.po index ccc9d0fd4..d2d96461b 100644 --- a/po/de.po +++ b/po/de.po @@ -816,6 +816,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/po/en.po b/po/en.po index d5ef4e551..974b4bf2c 100644 --- a/po/en.po +++ b/po/en.po @@ -814,6 +814,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/po/fr.po b/po/fr.po index d55a071e5..5051edbb4 100644 --- a/po/fr.po +++ b/po/fr.po @@ -915,6 +915,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/po/pl.po b/po/pl.po index 5c7d1d72e..c40bf1a9b 100644 --- a/po/pl.po +++ b/po/pl.po @@ -810,6 +810,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/po/pt_BR.po b/po/pt_BR.po index 0deaf0091..6c9438b65 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -815,6 +815,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/po/sv.po b/po/sv.po index 034bb2136..1abba7bf8 100644 --- a/po/sv.po +++ b/po/sv.po @@ -811,6 +811,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/po/zh_CN.po b/po/zh_CN.po index 20947dda4..92e14262a 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -808,6 +808,11 @@ msgstr "" msgid "%s and %s are mutually exclusive" msgstr "" +# +#, c-format +msgid "%s could not read response to primary device attribute query after waiting for %d seconds. This is often due to a missing feature in your terminal. See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. This %s process will no longer wait for outstanding queries, which disables some optional features." +msgstr "" + #, c-format msgid "%s, version %s" msgstr "" diff --git a/src/builtins/fish_key_reader.rs b/src/builtins/fish_key_reader.rs index e9e967b02..093ec9904 100644 --- a/src/builtins/fish_key_reader.rs +++ b/src/builtins/fish_key_reader.rs @@ -21,7 +21,7 @@ future_feature_flags, input_common::{ match_key_event_to_key, CharEvent, InputEventQueue, InputEventQueuer, KeyEvent, - QueryResponseEvent, TerminalQuery, + QueryResponse, QueryResultEvent, TerminalQuery, }, key::{char_to_symbol, Key}, nix::isatty, @@ -95,16 +95,17 @@ fn process_input(streams: &mut IoStreams, continuous_mode: bool, verbose: bool) handoff.enable_tty_protocols(); while (!first_char_seen || continuous_mode) && !check_exit_loop_maybe_warning(None) { + use QueryResultEvent::*; let kevt = match queue.readch() { CharEvent::Key(kevt) => kevt, CharEvent::Readline(_) | CharEvent::Command(_) | CharEvent::Implicit(_) => continue, - CharEvent::QueryResponse(QueryResponseEvent::PrimaryDeviceAttributeResponse) => { + CharEvent::QueryResult(Response(QueryResponse::PrimaryDeviceAttribute) | Timeout) => { if get_kitty_keyboard_capability() == Capability::Unknown { set_kitty_keyboard_capability(|| {}, Capability::NotSupported); } continue; } - CharEvent::QueryResponse(_) => continue, + CharEvent::QueryResult(_) => continue, }; if verbose { streams.out.append(L!("# decoded from: ")); diff --git a/src/fd_readable_set.rs b/src/fd_readable_set.rs index 2f02fab97..fbf303c1e 100644 --- a/src/fd_readable_set.rs +++ b/src/fd_readable_set.rs @@ -2,6 +2,7 @@ use std::os::unix::prelude::*; use std::time::Duration; +#[derive(Clone, Copy)] pub enum Timeout { Duration(Duration), Forever, diff --git a/src/input.rs b/src/input.rs index 945494204..c8cbf72f8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -781,7 +781,7 @@ pub fn read_char(&mut self) -> CharEvent { match evt { Key(_) => true, Implicit(Eof) => true, - Readline(_) | Command(_) | Implicit(_) | QueryResponse(_) => false, + Readline(_) | Command(_) | Implicit(_) | QueryResult(_) => false, } }); @@ -823,7 +823,7 @@ pub fn read_char(&mut self) -> CharEvent { self.push_front(evt); self.mapping_execute_matching_or_generic(); } - CharEvent::Implicit(_) | CharEvent::QueryResponse(_) => { + CharEvent::Implicit(_) | CharEvent::QueryResult(_) => { return evt; } } diff --git a/src/input_common.rs b/src/input_common.rs index c16aac5bf..021cdae75 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -1,10 +1,11 @@ use crate::common::{ fish_reserved_codepoint, is_windows_subsystem_for_linux, read_blocked, shell_modes, - str2wcstring, WSL, + str2wcstring, PROGRAM_NAME, WSL, }; use crate::env::{EnvStack, Environment}; use crate::fd_readable_set::{FdReadableSet, Timeout}; use crate::flog::{FloggableDebug, FloggableDisplay, FLOG}; +use crate::global_safety::RelaxedAtomicBool; use crate::key::{ self, alt, canonicalize_control_char, canonicalize_keyed_control_char, char_to_symbol, function_key, shift, Key, Modifiers, ViewportPosition, @@ -411,9 +412,15 @@ pub enum ImplicitEvent { } #[derive(Debug, Clone)] -pub enum QueryResponseEvent { - PrimaryDeviceAttributeResponse, - CursorPositionResponse(ViewportPosition), +pub enum QueryResponse { + PrimaryDeviceAttribute, + CursorPosition(ViewportPosition), +} + +#[derive(Debug, Clone)] +pub enum QueryResultEvent { + Response(QueryResponse), + Timeout, } #[derive(Debug, Clone)] @@ -430,7 +437,7 @@ pub enum CharEvent { /// Any event that has no user-visible representation. Implicit(ImplicitEvent), - QueryResponse(QueryResponseEvent), + QueryResult(QueryResultEvent), } impl FloggableDebug for CharEvent {} @@ -532,6 +539,9 @@ enum InputEventTrigger { // Our ioport reported a change, so service main thread requests. IOPortNotified, + + // No file descriptor was ready within the query timeout. + TimeoutElapsed, } fn readb(in_fd: RawFd) -> Option { @@ -547,7 +557,7 @@ fn readb(in_fd: RawFd) -> Option { Some(c) } -fn next_input_event(in_fd: RawFd) -> InputEventTrigger { +fn next_input_event(in_fd: RawFd, timeout: Timeout) -> InputEventTrigger { let mut fdset = FdReadableSet::new(); loop { fdset.clear(); @@ -565,7 +575,7 @@ fn next_input_event(in_fd: RawFd) -> InputEventTrigger { } // Here's where we call select(). - let select_res = fdset.check_readable(Timeout::Forever); + let select_res = fdset.check_readable(timeout); if select_res < 0 { let err = errno::errno().0; if err == libc::EINTR || err == libc::EAGAIN { @@ -576,6 +586,10 @@ fn next_input_event(in_fd: RawFd) -> InputEventTrigger { return InputEventTrigger::Eof; } } + if select_res == 0 { + assert!(!matches!(timeout, Timeout::Forever)); + return InputEventTrigger::TimeoutElapsed; + } // select() did not return an error, so we may have a readable fd. // The priority order is: uvars, stdin, ioport. @@ -787,7 +801,7 @@ fn try_pop(&mut self) -> Option { if self.is_blocked_querying() { use ImplicitEvent::*; match self.get_input_data().queue.front()? { - CharEvent::QueryResponse(_) | CharEvent::Implicit(CheckExit | Eof) => {} + CharEvent::QueryResult(_) | CharEvent::Implicit(CheckExit | Eof) => {} CharEvent::Key(_) | CharEvent::Readline(_) | CharEvent::Command(_) @@ -817,7 +831,26 @@ fn readch(&mut self) -> CharEvent { return mevt; } - match next_input_event(self.get_in_fd()) { + const INITIAL_QUERY_TIMEOUT_SECONDS: u64 = 2; + static ABANDON_WAITING_FOR_QUERIES: RelaxedAtomicBool = RelaxedAtomicBool::new(false); + + match next_input_event( + self.get_in_fd(), + if self.is_blocked_querying() { + Timeout::Duration(if ABANDON_WAITING_FOR_QUERIES.load() { + // This should small enough so the delay on incompatible terminals is + // not noticeable, and high enough so we can still receive some query + // responses if we ever get into this state on a compatible terminal, + // which can happen after extreme (network) latency exceeds our initial + // timeout. In future, we should tolerate this better. + Duration::from_millis(30) + } else { + Duration::from_secs(INITIAL_QUERY_TIMEOUT_SECONDS) + }) + } else { + Timeout::Forever + }, + ) { InputEventTrigger::Eof => { return CharEvent::Implicit(ImplicitEvent::Eof); } @@ -859,10 +892,12 @@ fn readch(&mut self) -> CharEvent { let mut i = 0; let ok = loop { if i == buffer.len() { - buffer.push(match next_input_event(self.get_in_fd()) { - InputEventTrigger::Byte(b) => b, - _ => 0, - }); + buffer.push( + match next_input_event(self.get_in_fd(), Timeout::Forever) { + InputEventTrigger::Byte(b) => b, + _ => 0, + }, + ); } match decode_input_byte( &mut seq, @@ -935,6 +970,26 @@ fn readch(&mut self) -> CharEvent { extra.map(|extra| self.insert_front(extra)); return key_evt; } + InputEventTrigger::TimeoutElapsed => { + if !ABANDON_WAITING_FOR_QUERIES.load() { + let program = PROGRAM_NAME.get().unwrap(); + FLOG!( + warning, + wgettext_fmt!( + "%s could not read response to primary device attribute query after waiting for %d seconds. \ + This is often due to a missing feature in your terminal. \ + See 'help terminal-compatibility' or 'man fish-terminal-compatibility'. \ + This %s process will no longer wait for outstanding queries, \ + which disables some optional features.", + program, + INITIAL_QUERY_TIMEOUT_SECONDS, + program + ), + ); + ABANDON_WAITING_FOR_QUERIES.store(true); + } + return CharEvent::QueryResult(QueryResultEvent::Timeout); + } } } } @@ -1176,9 +1231,9 @@ fn parse_csi(&mut self, buffer: &mut Vec) -> Option { }; FLOG!(reader, "Received cursor position report y:", y, "x:", x); let cursor_pos = ViewportPosition { x, y }; - self.push_front(CharEvent::QueryResponse( - QueryResponseEvent::CursorPositionResponse(cursor_pos), - )); + self.push_front(CharEvent::QueryResult(QueryResultEvent::Response( + QueryResponse::CursorPosition(cursor_pos), + ))); return None; } b'S' => masked_key(function_key(4)), @@ -1229,9 +1284,9 @@ fn parse_csi(&mut self, buffer: &mut Vec) -> Option { _ => return None, }, b'c' if private_mode == Some(b'?') => { - self.push_front(CharEvent::QueryResponse( - QueryResponseEvent::PrimaryDeviceAttributeResponse, - )); + self.push_front(CharEvent::QueryResult(QueryResultEvent::Response( + QueryResponse::PrimaryDeviceAttribute, + ))); return None; } b'u' => { diff --git a/src/reader.rs b/src/reader.rs index 66d36a4fb..c598d6af8 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -86,9 +86,10 @@ SearchFlags, SearchType, }; use crate::input::init_input; +use crate::input_common::QueryResponse; use crate::input_common::{ stop_query, CharEvent, CharInputStyle, CursorPositionQuery, CursorPositionQueryKind, - ImplicitEvent, InputData, QueryResponseEvent, ReadlineCmd, TerminalQuery, + ImplicitEvent, InputData, QueryResultEvent, ReadlineCmd, TerminalQuery, }; use crate::io::IoChain; use crate::key::ViewportPosition; @@ -2569,12 +2570,13 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo ); } }, - CharEvent::QueryResponse(query_result) => { + CharEvent::QueryResult(query_result) => { let mut maybe_query = self.blocking_query(); let query = &mut maybe_query; - use QueryResponseEvent::*; + use QueryResponse::*; + use QueryResultEvent::*; let query = match (&mut **query, query_result) { - (Some(TerminalQuery::Initial), PrimaryDeviceAttributeResponse) => { + (Some(TerminalQuery::Initial), Response(PrimaryDeviceAttribute) | Timeout) => { if get_kitty_keyboard_capability() == Capability::Unknown { set_kitty_keyboard_capability( reader_save_screen_state, @@ -2585,14 +2587,14 @@ fn handle_char_event(&mut self, injected_event: Option) -> ControlFlo } ( Some(TerminalQuery::CursorPosition(cursor_pos_query)), - CursorPositionResponse(cursor_pos), + Response(CursorPosition(cursor_pos)), ) => { cursor_pos_query.result = Some(cursor_pos); maybe_query } ( Some(TerminalQuery::CursorPosition(cursor_pos_query)), - PrimaryDeviceAttributeResponse, + Response(PrimaryDeviceAttribute) | Timeout, ) => { let cursor_pos_query = cursor_pos_query.clone(); drop(maybe_query);