Skip to content

Commit b4b9bd3

Browse files
authored
Kitty cursor backend selection (#778)
Fixes #767 Kitty needs to the terminal emulator's cursor to be visible so that it doesnt prompt you when you close the window. But if you use the flyline cursor backend, we hide the terminal emulator's cursor so that we can draw cool cursors. Other term ems don't seem to set this.
1 parent 45b3117 commit b4b9bd3

6 files changed

Lines changed: 85 additions & 11 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,13 @@ flyline set-cursor --help
473473
```
474474

475475
# Terminal emulator notes
476+
## Kitty:
477+
When running inside Kitty, it is highly recommended to use the terminal cursor backend:
478+
```bash
479+
flyline set-cursor --backend terminal
480+
```
481+
By default, Flyline detects if it is running in Kitty and defaults to the `terminal` backend. Using the custom `flyline` cursor backend inside Kitty hides the terminal's native hardware cursor, which prevents Kitty from detecting shell prompts, causing it to ask for confirmation when closing the terminal window.
482+
476483
## VS Code:
477484
Recommended settings
478485
- [`terminal.integrated.minimumContrastRatio = 1`](vscode://settings/terminal.integrated.minimumContrastRatio) to prevent the cell's foreground colour changing when it's under the cursor.

src/app/ui.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ impl<'a> App<'a> {
732732
cursor_pos
733733
};
734734
let cursor_style = {
735-
if self.settings.cursor_config.backend == CursorBackend::Terminal {
735+
if self.settings.cursor_config.backend() == CursorBackend::Terminal {
736736
None
737737
} else {
738738
let focused = self.term_has_focus
@@ -1528,7 +1528,7 @@ impl<'a> App<'a> {
15281528
};
15291529

15301530
if let Some(term_em_cursor) = drawn_content.term_em_cursor_pos()
1531-
&& (self.settings.cursor_config.backend == CursorBackend::Terminal
1531+
&& (self.settings.cursor_config.backend() == CursorBackend::Terminal
15321532
|| !self.mode.is_running())
15331533
&& !(self.mouse_state.is_left_button_down()
15341534
&& self.buffer.selection_range().is_some()

src/cli.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,10 +1324,27 @@ impl Flyline {
13241324
effect_speed,
13251325
effect_easing,
13261326
}) => {
1327+
// If the user configures flyline-only options without explicitly setting the backend,
1328+
// and we defaulted to terminal (e.g. on kitty), automatically switch to flyline.
1329+
if backend.is_none()
1330+
&& self.settings.cursor_config.is_backend_unset()
1331+
&& (style.is_some()
1332+
|| effect.is_some()
1333+
|| effect_speed.is_some()
1334+
|| effect_easing.is_some())
1335+
{
1336+
log::info!(
1337+
"Auto-switching cursor backend to Flyline for configured options"
1338+
);
1339+
self.settings
1340+
.cursor_config
1341+
.set_backend(Some(cursor::CursorBackend::Flyline));
1342+
}
1343+
13271344
// set backend first since it affects the validity of other options
13281345
if let Some(b) = backend {
13291346
log::info!("Cursor backend set to {:?}", b);
1330-
self.settings.cursor_config.backend = b;
1347+
self.settings.cursor_config.set_backend(Some(b));
13311348
if b == cursor::CursorBackend::Terminal
13321349
&& (style.is_some()
13331350
|| effect.is_some()
@@ -1342,8 +1359,8 @@ impl Flyline {
13421359

13431360
// Helper closure: every flyline-only option emits the same error.
13441361
// Returning a `bool` lets callers chain it with the option-presence check.
1345-
let backend_is_terminal =
1346-
self.settings.cursor_config.backend == cursor::CursorBackend::Terminal;
1362+
let backend_is_terminal = self.settings.cursor_config.backend()
1363+
== cursor::CursorBackend::Terminal;
13471364

13481365
if let Some(interp_str) = interpolate {
13491366
if interp_str.eq_ignore_ascii_case("none") {

src/cursor.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,9 @@ pub enum CursorStyleConfig {
190190
/// Complete cursor configuration set by `flyline set-cursor`.
191191
#[derive(Debug, Clone)]
192192
pub struct CursorConfig {
193-
/// Which backend renders the cursor.
194-
pub backend: CursorBackend,
193+
/// Which backend renders the cursor. If `None`, the default is resolved
194+
/// dynamically based on terminal emulator checks.
195+
backend: Option<CursorBackend>,
195196
/// Interpolation speed. `None` disables position
196197
/// interpolation and the cursor jumps instantly to its target.
197198
/// Default is `Some(16.0)`.
@@ -208,10 +209,43 @@ pub struct CursorConfig {
208209
pub effect_easing: CursorEasing,
209210
}
210211

212+
static IS_KITTY: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
213+
let term = crate::bash_funcs::get_envvar_value("TERM").unwrap_or_default();
214+
let term_program = crate::bash_funcs::get_envvar_value("TERM_PROGRAM").unwrap_or_default();
215+
term.to_lowercase().contains("xterm-kitty") || term_program.to_lowercase().contains("kitty")
216+
});
217+
218+
fn detect_kitty() -> bool {
219+
*IS_KITTY
220+
}
221+
222+
impl CursorConfig {
223+
/// Resolves the cursor backend to use, defaulting to `Terminal` on Kitty and `Flyline` otherwise.
224+
pub fn backend(&self) -> CursorBackend {
225+
self.backend.unwrap_or_else(|| {
226+
if detect_kitty() {
227+
CursorBackend::Terminal
228+
} else {
229+
CursorBackend::Flyline
230+
}
231+
})
232+
}
233+
234+
/// Sets the cursor rendering backend.
235+
pub fn set_backend(&mut self, backend: Option<CursorBackend>) {
236+
self.backend = backend;
237+
}
238+
239+
/// Returns `true` if no backend has been explicitly configured.
240+
pub fn is_backend_unset(&self) -> bool {
241+
self.backend.is_none()
242+
}
243+
}
244+
211245
impl Default for CursorConfig {
212246
fn default() -> Self {
213247
Self {
214-
backend: CursorBackend::Flyline,
248+
backend: None,
215249
interpolate: Some(16.0),
216250
interpolate_easing: CursorEasing::Linear,
217251
style: CursorStyleConfig::Default,

src/dparser.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,13 +1503,22 @@ mod tests {
15031503
// 14: "))", kind: DoubleRParen, matches with 2
15041504

15051505
assert_eq!(tokens[2].token.kind, TokenKind::ArithSubst);
1506-
assert_eq!(tokens[2].annotations.opening, Some(OpeningState::Matched(14)));
1506+
assert_eq!(
1507+
tokens[2].annotations.opening,
1508+
Some(OpeningState::Matched(14))
1509+
);
15071510

15081511
assert_eq!(tokens[4].token.kind, TokenKind::LParen);
1509-
assert_eq!(tokens[4].annotations.opening, Some(OpeningState::Matched(12)));
1512+
assert_eq!(
1513+
tokens[4].annotations.opening,
1514+
Some(OpeningState::Matched(12))
1515+
);
15101516

15111517
assert_eq!(tokens[5].token.kind, TokenKind::LParen);
1512-
assert_eq!(tokens[5].annotations.opening, Some(OpeningState::Matched(7)));
1518+
assert_eq!(
1519+
tokens[5].annotations.opening,
1520+
Some(OpeningState::Matched(7))
1521+
);
15131522

15141523
assert_eq!(tokens[7].token.kind, TokenKind::RParen);
15151524
assert_eq!(

src/tutorial.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,13 @@ pub fn generate_tutorial_text(
616616
TutorialStep::CursorStyleEffects => {
617617
lines.push(tl(Span::styled("Cursor Style & Effects", heading_style)));
618618
lines.push(empty());
619+
if detect_kitty_keyboard_support() {
620+
lines.push(tl(Span::styled(
621+
"⚠ Warning: You are running Kitty. The custom `flyline` cursor backend hides the terminal's native cursor, which stops Kitty from detecting prompt states and prompts you on exit. It is highly recommended to use the `terminal` backend instead.",
622+
ratatui::style::Style::default().fg(ratatui::style::Color::Red),
623+
)));
624+
lines.push(empty());
625+
}
619626
lines.push(TaggedLine::from(vec![
620627
TaggedSpan::new(Span::styled("Use ", text_style), Tag::Tutorial),
621628
ts_copiable(

0 commit comments

Comments
 (0)