feat: configure cursor appearance by editing mode
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
use crate::editor::Mode;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub(crate) struct CursorConfig {
|
||||
pub(crate) mode_colors: bool,
|
||||
pub(crate) normal: CursorShape,
|
||||
pub(crate) insert: CursorShape,
|
||||
pub(crate) command: CursorShape,
|
||||
pub(crate) search: CursorShape,
|
||||
pub(crate) visual: CursorShape,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum CursorShape {
|
||||
Block,
|
||||
Bar,
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl Default for CursorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mode_colors: true,
|
||||
normal: CursorShape::Block,
|
||||
insert: CursorShape::Block,
|
||||
command: CursorShape::Block,
|
||||
search: CursorShape::Block,
|
||||
visual: CursorShape::Block,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorConfig {
|
||||
pub(crate) fn shape(self, mode: Mode) -> CursorShape {
|
||||
match mode {
|
||||
Mode::Normal => self.normal,
|
||||
Mode::Insert => self.insert,
|
||||
Mode::Command => self.command,
|
||||
Mode::Search => self.search,
|
||||
Mode::Visual => self.visual,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2228,6 +2228,7 @@ pub struct Editor {
|
||||
bottom_bar: ChromeLine,
|
||||
popup: PopupConfig,
|
||||
labels: BTreeMap<String, String>,
|
||||
pub(crate) cursor_config: crate::cursor::CursorConfig,
|
||||
scroll_distance: Option<usize>,
|
||||
scroll_step_lines: Option<NonZeroUsize>,
|
||||
highlight_trailing_whitespace: bool,
|
||||
@@ -2353,6 +2354,7 @@ impl Editor {
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
labels: BTreeMap::new(),
|
||||
cursor_config: crate::cursor::CursorConfig::default(),
|
||||
scroll_distance: Some(0),
|
||||
scroll_step_lines: None,
|
||||
highlight_trailing_whitespace: false,
|
||||
|
||||
@@ -2,6 +2,7 @@ mod battery;
|
||||
mod blame;
|
||||
pub mod chrome;
|
||||
pub mod clipboard;
|
||||
pub(crate) mod cursor;
|
||||
pub mod diff;
|
||||
pub mod editor;
|
||||
pub mod files;
|
||||
|
||||
@@ -43,6 +43,7 @@ pub struct Config {
|
||||
pub clipboard_mode: String,
|
||||
pub git_blame: bool,
|
||||
pub git_gutter: crate::gutter::GutterStyle,
|
||||
pub(crate) cursor: crate::cursor::CursorConfig,
|
||||
pub top_bar: ChromeLine,
|
||||
pub bottom_bar: ChromeLine,
|
||||
pub popup: PopupConfig,
|
||||
@@ -101,6 +102,7 @@ impl Default for Config {
|
||||
clipboard_mode: "system".into(),
|
||||
git_blame: true,
|
||||
git_gutter: crate::gutter::GutterStyle::default(),
|
||||
cursor: crate::cursor::CursorConfig::default(),
|
||||
top_bar: ChromeLine::top(),
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
@@ -146,6 +148,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
clipboard_mode,
|
||||
git_blame,
|
||||
git_gutter,
|
||||
cursor,
|
||||
top_bar,
|
||||
bottom_bar,
|
||||
popup,
|
||||
@@ -192,6 +195,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
config.clipboard_mode = clipboard_mode;
|
||||
config.git_blame = git_blame;
|
||||
config.git_gutter = git_gutter;
|
||||
config.cursor = cursor;
|
||||
config.top_bar = top_bar;
|
||||
config.bottom_bar = bottom_bar;
|
||||
config.popup = popup;
|
||||
@@ -558,6 +562,8 @@ struct ConfigFrame {
|
||||
clipboard_mode: String,
|
||||
git_blame: bool,
|
||||
git_gutter: crate::gutter::GutterStyle,
|
||||
#[serde(default)]
|
||||
cursor: crate::cursor::CursorConfig,
|
||||
#[serde(deserialize_with = "crate::chrome::top")]
|
||||
top_bar: ChromeLine,
|
||||
#[serde(deserialize_with = "crate::chrome::bottom")]
|
||||
|
||||
@@ -142,6 +142,7 @@ module Vamp
|
||||
@clipboard_mode = "system"
|
||||
@git_blame = true
|
||||
@git_gutter = "bar"
|
||||
@cursor = {}
|
||||
@bar = BarConfig.new
|
||||
@popup = PopupConfig.new
|
||||
@labels = {}
|
||||
@@ -231,6 +232,32 @@ module Vamp
|
||||
@git_gutter = style
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Boolean, String, Symbol}] mode_colors defaults to true; mode shapes default to block
|
||||
# @return [Hash{String => Boolean, String}]
|
||||
def cursor=(options)
|
||||
raise TypeError, "cursor options must be a Hash" unless options.is_a?(Hash)
|
||||
|
||||
@cursor = options.each_with_object({}) do |(option, value), result|
|
||||
option = option.to_s if option.is_a?(Symbol)
|
||||
option = Vamp.string!(option, "cursor option")
|
||||
raise ArgumentError, "duplicate cursor option: #{option}" if result.key?(option)
|
||||
|
||||
case option
|
||||
when "mode_colors"
|
||||
raise TypeError, "cursor mode_colors must be true or false" unless [true, false].include?(value)
|
||||
when "normal", "insert", "command", "search", "visual"
|
||||
value = value.to_s if value.is_a?(Symbol)
|
||||
value = Vamp.string!(value, "cursor shape")
|
||||
unless %w[block bar underline].include?(value)
|
||||
raise ArgumentError, "cursor shape must be block, bar, or underline"
|
||||
end
|
||||
else
|
||||
raise ArgumentError, "unknown cursor option: #{option}"
|
||||
end
|
||||
result[option] = value
|
||||
end
|
||||
end
|
||||
|
||||
# @param values [Hash{String, Symbol => String}]
|
||||
# @return [Hash]
|
||||
def labels=(values)
|
||||
@@ -441,6 +468,7 @@ module Vamp
|
||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||
"git_blame" => @git_blame,
|
||||
"git_gutter" => @git_gutter,
|
||||
"cursor" => @cursor,
|
||||
"scroll_distance" => @scroll_distance,
|
||||
"scroll_step_lines" => @scroll_step_lines,
|
||||
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
|
||||
|
||||
@@ -516,6 +516,7 @@ impl EditorSession {
|
||||
}),
|
||||
None => Config::default(),
|
||||
};
|
||||
editor.cursor_config = config.cursor;
|
||||
let (pull_request_preferences_store, mut preferences, notice) = match preferences_path {
|
||||
Some(path) => crate::pull_request_preferences::Store::load(path),
|
||||
None => (
|
||||
|
||||
+79
-6
@@ -14,7 +14,7 @@ use crossterm::event::{
|
||||
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
cursor::{SetCursorStyle, Show},
|
||||
event::{
|
||||
self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
|
||||
@@ -98,6 +98,7 @@ fn restore_terminal() {
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
ColorSchemeNotifications(false),
|
||||
ResetCursor,
|
||||
LeaveAlternateScreen,
|
||||
Show
|
||||
);
|
||||
@@ -169,7 +170,8 @@ impl TerminalSession {
|
||||
DisableBracketedPaste,
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
ColorSchemeNotifications(false)
|
||||
ColorSchemeNotifications(false),
|
||||
ResetCursor
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1290,10 +1292,64 @@ fn wheel_kind(mouse: &MouseEvent) -> Option<MouseEventKind> {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
struct CursorAppearance {
|
||||
shape: SetCursorStyle,
|
||||
color: Option<[u8; 3]>,
|
||||
}
|
||||
|
||||
fn mode_cursor_color(mode: Mode) -> [u8; 3] {
|
||||
match mode {
|
||||
Mode::Normal => [95, 215, 255],
|
||||
Mode::Insert => [135, 215, 135],
|
||||
Mode::Visual => [215, 135, 255],
|
||||
Mode::Command => [255, 215, 95],
|
||||
Mode::Search => [255, 175, 95],
|
||||
}
|
||||
}
|
||||
|
||||
impl CursorAppearance {
|
||||
fn for_editor(editor: &Editor) -> Self {
|
||||
use crate::cursor::CursorShape;
|
||||
Self {
|
||||
shape: match editor.cursor_config.shape(editor.mode()) {
|
||||
CursorShape::Block => SetCursorStyle::SteadyBlock,
|
||||
CursorShape::Bar => SetCursorStyle::SteadyBar,
|
||||
CursorShape::Underline => SetCursorStyle::SteadyUnderScore,
|
||||
},
|
||||
color: editor
|
||||
.cursor_config
|
||||
.mode_colors
|
||||
.then(|| mode_cursor_color(editor.mode())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crossterm::Command for CursorAppearance {
|
||||
fn write_ansi(&self, output: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
self.shape.write_ansi(output)?;
|
||||
if let Some([red, green, blue]) = self.color {
|
||||
write!(output, "\x1b]12;#{red:02x}{green:02x}{blue:02x}\x1b\\")
|
||||
} else {
|
||||
output.write_str("\x1b]112\x1b\\")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResetCursor;
|
||||
|
||||
impl crossterm::Command for ResetCursor {
|
||||
fn write_ansi(&self, output: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
SetCursorStyle::DefaultUserShape.write_ansi(output)?;
|
||||
output.write_str("\x1b]112\x1b\\")
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_frame<W: io::Write>(
|
||||
terminal: &mut Terminal<CrosstermBackend<Vec<u8>>>,
|
||||
output: &mut W,
|
||||
clear_screen: bool,
|
||||
cursor: Option<CursorAppearance>,
|
||||
render: impl FnOnce(&mut Frame),
|
||||
) -> io::Result<()> {
|
||||
// Do not suspend the terminal's presentation while computing the next frame.
|
||||
@@ -1301,6 +1357,9 @@ fn draw_frame<W: io::Write>(
|
||||
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
|
||||
// Terminals without synchronized updates still hide the painting cursor.
|
||||
let result = terminal.hide_cursor().and_then(|()| {
|
||||
if let Some(cursor) = cursor {
|
||||
crossterm::queue!(terminal.backend_mut(), cursor)?;
|
||||
}
|
||||
if clear_screen {
|
||||
// Fresh Ratatui buffers are already empty; avoid its blocking cursor query.
|
||||
crossterm::queue!(
|
||||
@@ -1326,6 +1385,7 @@ pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()
|
||||
let mut query_theme = true;
|
||||
let mut redraw = true;
|
||||
let mut pending = None;
|
||||
let mut cursor = None;
|
||||
while !session.editor().should_quit() {
|
||||
redraw |= session.poll();
|
||||
colors.expire(Instant::now());
|
||||
@@ -1334,9 +1394,15 @@ pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
draw_frame(&mut terminal, &mut output, clear_screen, |frame| {
|
||||
viewports.draw_session(frame, session)
|
||||
})?;
|
||||
let appearance = CursorAppearance::for_editor(session.editor());
|
||||
draw_frame(
|
||||
&mut terminal,
|
||||
&mut output,
|
||||
clear_screen,
|
||||
(cursor != Some(appearance)).then_some(appearance),
|
||||
|frame| viewports.draw_session(frame, session),
|
||||
)?;
|
||||
cursor = Some(appearance);
|
||||
redraw = false;
|
||||
clear_screen = false;
|
||||
}
|
||||
@@ -3312,7 +3378,14 @@ fn paint_editor_line(
|
||||
left,
|
||||
column,
|
||||
column.saturating_add(1),
|
||||
Style::default().fg(Color::Black).bg(Color::Magenta),
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(if editor.cursor_config.mode_colors {
|
||||
let [red, green, blue] = mode_cursor_color(editor.mode());
|
||||
Color::Rgb(red, green, blue)
|
||||
} else {
|
||||
Color::Magenta
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user