From bff1a54ee765c8d71c0d6105d9be0ecf496033d1 Mon Sep 17 00:00:00 2001 From: Alxy Savin Date: Sat, 14 Mar 2026 01:32:10 +0300 Subject: [PATCH] Add configurable terminal cursor shape --- config.example.toml | 1 + docs/iamb.5 | 10 ++++++++++ src/config.rs | 24 ++++++++++++++++++++++++ src/main.rs | 19 ++++++++++++++++--- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/config.example.toml b/config.example.toml index c4ebd653..2d6f2ba1 100644 --- a/config.example.toml +++ b/config.example.toml @@ -5,6 +5,7 @@ user_id = "@user:matrix.org" url = "https://matrix.org" [settings] +cursor_shape = "default" default_room = "#iamb-users:0x.badd.cafe" external_edit_file_suffix = ".md" log_level = "warn" diff --git a/docs/iamb.5 b/docs/iamb.5 index 8357f965..10536ed1 100644 --- a/docs/iamb.5 +++ b/docs/iamb.5 @@ -128,6 +128,16 @@ key and can be overridden as described in .It Sy external_edit_file_suffix Suffix to append to temporary file names when using the :editor command. Defaults to .md. +.It Sy cursor_shape +Control the terminal cursor shape used by iamb. +Possible values are: +.Dq Sy default , +.Dq Sy block , +.Dq Sy line , and +.Dq Sy underline . +Defaults to +.Dq Sy default . + .It Sy default_room The room to show by default instead of the .Sy :welcome diff --git a/src/config.rs b/src/config.rs index e7a4a47b..0ec42ee2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -558,6 +558,7 @@ impl SortOverrides { #[derive(Clone)] pub struct TunableValues { + pub cursor_shape: CursorShape, pub log_level: Level, pub message_shortcode_display: bool, pub normal_after_send: bool, @@ -585,6 +586,7 @@ pub struct TunableValues { #[derive(Clone, Default, Deserialize)] pub struct Tunables { + pub cursor_shape: Option, pub log_level: Option, pub message_shortcode_display: Option, pub normal_after_send: Option, @@ -614,6 +616,7 @@ pub struct Tunables { impl Tunables { fn merge(self, other: Self) -> Self { Tunables { + cursor_shape: self.cursor_shape.or(other.cursor_shape), log_level: self.log_level.or(other.log_level), message_shortcode_display: self .message_shortcode_display @@ -648,6 +651,7 @@ impl Tunables { fn values(self) -> TunableValues { TunableValues { + cursor_shape: self.cursor_shape.unwrap_or_default(), log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO), message_shortcode_display: self.message_shortcode_display.unwrap_or(false), normal_after_send: self.normal_after_send.unwrap_or(false), @@ -677,6 +681,16 @@ impl Tunables { } } +#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum CursorShape { + #[default] + Default, + Block, + Line, + Underline, +} + #[derive(Clone)] pub struct DirectoryValues { pub cache: PathBuf, @@ -1345,6 +1359,16 @@ mod tests { assert!(serde_json::from_str::(r#""""#).is_err()); } + #[test] + fn test_parse_cursor_shape() { + assert_eq!(CursorShape::Default, CursorShape::default()); + assert_eq!(CursorShape::Default, serde_json::from_str(r#""default""#).unwrap()); + assert_eq!(CursorShape::Block, serde_json::from_str(r#""block""#).unwrap()); + assert_eq!(CursorShape::Line, serde_json::from_str(r#""line""#).unwrap()); + assert_eq!(CursorShape::Underline, serde_json::from_str(r#""underline""#).unwrap()); + assert!(serde_json::from_str::(r#""beam""#).is_err()); + } + #[test] fn test_load_example_config_toml() { let path = PathBuf::from("config.example.toml"); diff --git a/src/main.rs b/src/main.rs index 0a316c76..dce2a3ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ use tracing_subscriber::FmtSubscriber; use modalkit::crossterm::{ self, - cursor::Show as CursorShow, + cursor::{SetCursorStyle, Show as CursorShow}, event::{ poll, read, @@ -99,7 +99,7 @@ use crate::{ ProgramContext, ProgramStore, }, - config::{ApplicationSettings, Iamb}, + config::{ApplicationSettings, CursorShape, Iamb}, windows::IambWindow, worker::{create_room, ClientWorker, LoginStyle, Requester}, }; @@ -976,6 +976,12 @@ async fn login_normal( /// Set up the terminal for drawing the TUI, and getting additional info. fn setup_tty(settings: &ApplicationSettings, enable_enhanced_keys: bool) -> std::io::Result<()> { let title = format!("iamb ({})", settings.profile.user_id.as_str()); + let cursor_style = match settings.tunables.cursor_shape { + CursorShape::Default => SetCursorStyle::DefaultUserShape, + CursorShape::Block => SetCursorStyle::SteadyBlock, + CursorShape::Line => SetCursorStyle::SteadyBar, + CursorShape::Underline => SetCursorStyle::SteadyUnderScore, + }; // Enable raw mode and enter the alternate screen. crossterm::terminal::enable_raw_mode()?; @@ -993,7 +999,13 @@ fn setup_tty(settings: &ApplicationSettings, enable_enhanced_keys: bool) -> std: crossterm::execute!(stdout(), EnableMouseCapture)?; } - crossterm::execute!(stdout(), EnableBracketedPaste, EnableFocusChange, SetTitle(title)) + crossterm::execute!( + stdout(), + EnableBracketedPaste, + EnableFocusChange, + SetTitle(title), + cursor_style + ) } // Do our best to reverse what we did in setup_tty() when we exit or crash. @@ -1010,6 +1022,7 @@ fn restore_tty(enable_enhanced_keys: bool, enable_mouse: bool) { stdout(), DisableBracketedPaste, DisableFocusChange, + SetCursorStyle::DefaultUserShape, LeaveAlternateScreen, CursorShow, );