Skip to content

Commit

Permalink
fix(input): handle wide characters
Browse files Browse the repository at this point in the history
  • Loading branch information
Beastwick18 committed Jun 22, 2024
1 parent 7322023 commit ba7a4ee
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 68 deletions.
30 changes: 30 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ color-to-tui = { version = "0.3.0", default-features = false }
human_bytes = { version = "0.4.3", default-features = false }
strum = { version = "0.26.2", default-features = false }
ratatui-image = { version = "1.0.1", optional = true , default-features = false }
image = { version = "0.25.1", optional = true , default-features = false }
image = { version = "0.25.1", optional = true, features = ["png"], default-features = false }
base64 = { version = "0.22.1", features = ["alloc"], default-features = false }
lexopt = "0.3.0"

Expand Down
3 changes: 2 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,14 +658,15 @@ impl App {
Some(imdb) => imdb,
None => return ctx.show_error("No imdb ID found for this item."),
},
'n' => item.title,
_ => return,
};
match clipboard.try_copy(&link) {
Ok(()) => ctx.notify(format!("Copied \"{}\" to clipboard", link)),
Err(e) => ctx.show_error(e),
}
}
None if ['t', 'm', 'p', 'i'].contains(&c) => {
None if ['t', 'm', 'p', 'i', 'n'].contains(&c) => {
ctx.show_error("Failed to copy:\nFailed to get item")
}
None => {}
Expand Down
4 changes: 3 additions & 1 deletion src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,9 @@ impl Sources {
w.filter.table.select(w.filter.selected);

w.search.input.input = self.default_search(&ctx.config.sources);
w.search.input.cursor = w.search.input.input.len();
w.search
.input
.set_cursor(w.search.input.input.chars().count());

// Go back to first page when changing source
ctx.page = 1;
Expand Down
7 changes: 0 additions & 7 deletions src/widget/captcha.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ impl Default for CaptchaPopup {
}
}

impl InputWidget {
pub fn clear(&mut self) {
self.input.clear();
self.cursor = 0;
}
}

impl Widget for CaptchaPopup {
fn draw(&mut self, f: &mut Frame, ctx: &Context, area: Rect) {
let center = area.inner(&Margin {
Expand Down
186 changes: 134 additions & 52 deletions src/widget/input.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::cmp::{max, min};
use std::{
cmp::{max, min},
ops::RangeBounds,
};

use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::{
Expand All @@ -12,6 +15,7 @@ use crate::app::Context;

pub struct InputWidget {
pub input: String,
pub char_idx: usize,
pub cursor: usize,
pub max_len: usize,
pub validator: Option<fn(&char) -> bool>,
Expand All @@ -21,6 +25,7 @@ impl InputWidget {
pub fn new(max_len: usize, validator: Option<fn(&char) -> bool>) -> Self {
InputWidget {
input: "".to_owned(),
char_idx: 0,
cursor: 0,
max_len,
validator,
Expand All @@ -33,6 +38,42 @@ impl InputWidget {
area.y,
);
}

pub fn set_cursor(&mut self, idx: usize) {
self.char_idx = idx.max(self.max_len);
self.cursor = pos_of_nth_char(&self.input, self.char_idx);
}

pub fn clear(&mut self) {
self.input.clear();
self.cursor = 0;
self.char_idx = 0;
}
}

fn pos_of_nth_char(s: &String, idx: usize) -> usize {
s.chars()
.take(idx)
.fold(0, |acc, c| acc + c.width().unwrap_or(0))
}

fn without_nth_char(s: &String, idx: usize) -> String {
s.chars()
.enumerate()
.filter_map(|(i, c)| if i != idx { Some(c) } else { None })
.collect::<String>()
}

fn without_range(s: &String, range: impl RangeBounds<usize>) -> String {
let mut vec = s.chars().collect::<Vec<char>>();
vec.drain(range);
vec.into_iter().collect()
}

fn insert_char(s: &String, idx: usize, x: char) -> String {
let mut vec = s.chars().collect::<Vec<char>>();
vec.insert(idx, x);
vec.into_iter().collect()
}

impl super::Widget for InputWidget {
Expand Down Expand Up @@ -69,91 +110,132 @@ impl super::Widget for InputWidget {
return; // If character is invalid, ignore it
}
}
if self.input.len() < self.max_len {
self.input.insert(self.cursor, *c);
self.cursor += c.width_cjk().unwrap_or(0);
if self.input.chars().count() < self.max_len {
self.input = insert_char(&self.input, self.char_idx, *c);
self.char_idx += 1;
}
}
(Char('b') | Left, &KeyModifiers::CONTROL) => {
let non_space = self.input[..min(self.cursor, self.input.len())]
.rfind(|item| item != ' ')
let cursor = min(self.char_idx, self.input.chars().count());
// Find the first non-space character before the cursor
let non_space = self
.input
.chars()
.take(cursor)
.collect::<Vec<char>>()
.into_iter()
.rposition(|c| c != ' ')
.unwrap_or(0);

// Find the first space character before the first non-space character
self.char_idx = self
.input
.chars()
.take(non_space)
.collect::<Vec<char>>()
.into_iter()
.rposition(|c| c == ' ')
.map(|u| u + 1)
.unwrap_or(0);
self.cursor = match self.input[..non_space].rfind(|item| item == ' ') {
Some(pos) => pos + 1,
None => 0,
};
}
(Char('w') | Right, &KeyModifiers::CONTROL) => {
let idx = min(self.cursor + 1, self.input.len());
self.cursor = match self.input[idx..].find(|item| item == ' ') {
Some(pos) => self.cursor + pos + 2,
None => self.input.len(),
};
let idx = min(self.char_idx + 1, self.input.chars().count());

self.char_idx = self
.input
.chars()
.skip(idx)
.collect::<Vec<char>>()
.into_iter()
.position(|c| c == ' ')
.map(|u| self.char_idx + u + 2)
.unwrap_or(self.input.chars().count());
}
(Delete, &KeyModifiers::CONTROL | &KeyModifiers::ALT) => {
let idx = min(self.cursor + 1, self.input.len());
let new_cursor = match self.input[idx..].find(|item| item == ' ') {
Some(pos) => self.cursor + pos + 2,
None => self.input.len(),
};
self.input.replace_range(self.cursor..new_cursor, "");
let idx = min(self.char_idx + 1, self.input.chars().count());

let new_cursor = self
.input
.chars()
.skip(idx)
.collect::<Vec<char>>()
.into_iter()
.position(|c| c == ' ')
.map(|u| self.char_idx + u + 2)
.unwrap_or(self.input.chars().count());
self.input = without_range(&self.input, self.char_idx..new_cursor)
}
(Backspace, &KeyModifiers::CONTROL | &KeyModifiers::ALT) => {
let cursor = min(self.cursor, self.input.len());
let non_space = self.input[..cursor].rfind(|i| i != ' ').unwrap_or(0);
self.cursor = match self.input[..non_space].rfind(|i| i == ' ') {
Some(pos) => pos + 1,
None => 0,
};
self.input.replace_range(self.cursor..cursor, "");
let cursor = min(self.char_idx, self.input.chars().count());
// Find the first non-space character before the cursor
let non_space = self
.input
.chars()
.take(cursor)
.collect::<Vec<char>>()
.into_iter()
.rposition(|c| c != ' ')
.unwrap_or(0);

// Find the first space character before the first non-space character
self.char_idx = self
.input
.chars()
.take(non_space)
.collect::<Vec<char>>()
.into_iter()
.rposition(|c| c == ' ')
.map(|u| u + 1)
.unwrap_or(0);
self.input = without_range(&self.input, self.char_idx..cursor)
}
(Backspace, &KeyModifiers::NONE) => {
if !self.input.is_empty() && self.cursor > 0 {
self.input.remove(self.cursor - 1);
self.cursor -= 1;
if !self.input.is_empty() && self.char_idx > 0 {
self.char_idx -= 1;
self.input = without_nth_char(&self.input, self.char_idx);
}
}
(Delete, &KeyModifiers::NONE) => {
if !self.input.is_empty() && self.cursor < self.input.len() {
self.input.remove(self.cursor);
if !self.input.is_empty() && self.char_idx < self.input.chars().count() {
self.input = without_nth_char(&self.input, self.char_idx);
}
}
(Left, &KeyModifiers::NONE)
| (Char('h'), &KeyModifiers::CONTROL | &KeyModifiers::ALT) => {
self.cursor = max(self.cursor, 1) - 1;
self.char_idx = max(self.char_idx, 1) - 1;
}
(Right, &KeyModifiers::NONE)
| (Char('l'), &KeyModifiers::CONTROL | &KeyModifiers::ALT) => {
self.cursor = min(self.cursor + 1, self.input.len());
self.char_idx = min(self.char_idx + 1, self.input.chars().count());
}
(End, &KeyModifiers::NONE) | (Char('e'), &KeyModifiers::CONTROL) => {
self.cursor = self.input.len();
self.char_idx = self.input.chars().count();
}
(Home, &KeyModifiers::NONE) | (Char('a'), &KeyModifiers::CONTROL) => {
self.cursor = 0;
self.char_idx = 0;
}
(Char('u'), &KeyModifiers::CONTROL) => {
self.cursor = 0;
self.char_idx = 0;
"".clone_into(&mut self.input);
}
_ => {}
};
self.cursor = pos_of_nth_char(&self.input, self.char_idx);
}
if let Event::Paste(mut p) = evt.to_owned() {
if let Some(validator) = self.validator {
if let Event::Paste(p) = evt.to_owned() {
let space_left = self.max_len - self.input.chars().count();
let p = match self.validator {
// Remove invalid chars
p = p.chars().filter(validator).collect();
}
self.input = format!(
"{}{}{}",
&self.input[..self.cursor],
p,
&self.input[self.cursor..]
);
if self.input.len() > self.max_len {
self.input = self.input[..self.max_len].to_string();
}
self.cursor = min(self.cursor + p.len(), self.max_len);
Some(v) => p.chars().filter(v).collect(),
None => p,
};
let p: String = p.chars().take(space_left).collect();
let before: String = self.input.chars().take(self.char_idx).collect();
let after: String = self.input.chars().skip(self.char_idx).collect();
self.input = format!("{before}{p}{after}");
self.char_idx = min(self.char_idx + p.chars().count(), self.max_len);

self.cursor = pos_of_nth_char(&self.input, self.char_idx);
}
}

Expand Down
6 changes: 2 additions & 4 deletions src/widget/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ impl Widget for PagePopup {
KeyCode::Esc => {
ctx.mode = Mode::Normal;
// Clear input on Esc
self.input.input.clear();
self.input.cursor = 0;
self.input.clear();
}
KeyCode::Enter => {
ctx.page = max(
Expand All @@ -81,8 +80,7 @@ impl Widget for PagePopup {
ctx.mode = Mode::Loading(LoadType::Searching);

// Clear input on Enter
self.input.input.clear();
self.input.cursor = 0;
self.input.clear();
}
_ => {}
}
Expand Down
5 changes: 4 additions & 1 deletion src/widget/results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,10 @@ impl super::Widget for ResultsWidget {
("P, H", "First Page"),
("r", "Reload"),
("o", "Open in browser"),
("yt, ym, yp, yi", "Copy torrent/magnet/post/imdb id"),
(
"yt, ym, yp, yi, yn",
"Copy torrent/magnet/post link/imdb id/name",
),
("Space", "Toggle item for batch download"),
("Ctrl-Space", "Multi-line select torrents"),
("Tab/Shift-Tab", "Switch to Batches"),
Expand Down
Loading

0 comments on commit ba7a4ee

Please sign in to comment.