feat: show block cursors in active text inputs
This commit is contained in:
+65
-9
@@ -6,6 +6,8 @@
|
||||
//! Search and new-tag text: Option/Alt+Backspace or Ctrl-W deletes the previous whitespace word.
|
||||
//! Cmd/Super+Backspace or Ctrl-U clears the line; plain Backspace removes one character.
|
||||
//! The terminal must transmit modified Backspace; otherwise map it to Ctrl-W or Ctrl-U.
|
||||
//! Active text fields show a steady block cursor at the scrolled end; other views hide it.
|
||||
//! The terminal's configured default cursor shape is restored when the app exits.
|
||||
//! Normal: j/k selects, Enter streams, and i returns to insert mode to edit the query.
|
||||
//! Normal-mode d opens the full description; Up/Down scrolls lines and PgUp/PgDn scrolls pages.
|
||||
//! The centered popup uses up to 70% width and 60% height, capped at 96 by 20 cells.
|
||||
@@ -37,7 +39,11 @@ use std::{collections::BTreeMap, ffi::OsString, io, path::PathBuf, time::Duratio
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
DefaultTerminal, Frame,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
crossterm::{
|
||||
cursor::SetCursorStyle,
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
execute,
|
||||
},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
@@ -702,19 +708,28 @@ impl App {
|
||||
};
|
||||
// End-only editing; scroll long queries without counting UTF-8 bytes as columns.
|
||||
let query = ratatui::text::Line::raw(self.query.as_str());
|
||||
let scroll = query
|
||||
.width()
|
||||
.saturating_sub(search.width.saturating_sub(3) as usize);
|
||||
let query_width = query.width();
|
||||
let search_block = Block::bordered()
|
||||
.title(" tubular / YouTube search ")
|
||||
.border_style(search_style);
|
||||
let input = search_block.inner(search);
|
||||
let scroll = query_width.saturating_sub(input.width.saturating_sub(1) as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(query)
|
||||
.scroll((0, scroll.min(u16::MAX as usize) as u16))
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title(" tubular / YouTube search ")
|
||||
.border_style(search_style),
|
||||
),
|
||||
.block(search_block),
|
||||
search,
|
||||
);
|
||||
if self.mode == Mode::Insert
|
||||
&& self.category_dialog.is_none()
|
||||
&& input.width != 0
|
||||
&& input.height != 0
|
||||
{
|
||||
let cursor = query_width
|
||||
.saturating_sub(scroll)
|
||||
.min(usize::from(input.width - 1));
|
||||
frame.set_cursor_position((input.x + cursor as u16, input.y));
|
||||
}
|
||||
let view = &mut self.views[self.view_index];
|
||||
let table = Table::new(
|
||||
view.videos.iter().map(|video| {
|
||||
@@ -907,6 +922,14 @@ impl App {
|
||||
}
|
||||
|
||||
fn run(mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
struct ResetCursorStyle;
|
||||
impl Drop for ResetCursorStyle {
|
||||
fn drop(&mut self) {
|
||||
let _ = execute!(io::stdout(), SetCursorStyle::DefaultUserShape);
|
||||
}
|
||||
}
|
||||
execute!(io::stdout(), SetCursorStyle::SteadyBlock)?;
|
||||
let _cursor_style = ResetCursorStyle;
|
||||
self.thumbnail.query_terminal();
|
||||
if !self.query.trim().is_empty() {
|
||||
self.search();
|
||||
@@ -991,6 +1014,39 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_cursor_tracks_display_width_and_stays_inside_scrolled_fields() {
|
||||
use ratatui::{Terminal, backend::TestBackend};
|
||||
|
||||
let mut app = app(&["tubular"]);
|
||||
let mut terminal = Terminal::new(TestBackend::new(24, 20)).unwrap();
|
||||
for (text, x) in [
|
||||
("", 1),
|
||||
("ab", 3),
|
||||
("日本語", 7),
|
||||
("e\u{301}", 2),
|
||||
("abcdefghijklmnopqrstuvwxyz", 22),
|
||||
] {
|
||||
app.query = text.into();
|
||||
terminal.draw(|frame| app.draw(frame)).unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap(), (x, 2).into());
|
||||
}
|
||||
app.category_dialog = Some(CategoryDialog {
|
||||
channel: String::new(),
|
||||
handle: String::new(),
|
||||
label: String::new(),
|
||||
tags: BTreeMap::new(),
|
||||
selection: ListState::default(),
|
||||
name: Some(String::new()),
|
||||
error: None,
|
||||
});
|
||||
for (text, x) in [("", 3), ("日本語", 9), ("abcdefghijklmnopqrstuvwxyz", 20)] {
|
||||
app.category_dialog.as_mut().unwrap().name = Some(text.into());
|
||||
terminal.draw(|frame| app.draw(frame)).unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap(), (x, 7).into());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deletion_shortcuts_edit_search_and_tag_names_without_submitting() {
|
||||
for tag_input in [false, true] {
|
||||
|
||||
Reference in New Issue
Block a user