feat: add direct view shortcuts and category search

This commit is contained in:
2026-10-12 01:41:19 +02:00
parent b15fa23b8a
commit 28c94eb70b
4 changed files with 498 additions and 108 deletions
+6 -2
View File
@@ -110,7 +110,9 @@ actions! {
DeleteCharacter, "input.delete.character", "Delete the previous character", TEXT, ["backspace"];
DeleteWord, "input.delete.word", "Delete the previous word", TEXT, ["alt+backspace", "ctrl+w"];
DeleteLine, "input.delete.line", "Clear the input line", TEXT, ["super+backspace", "ctrl+u", "alt+super+backspace"];
SearchEdit, "search.edit", "Edit the video search", VIEW, ["i"];
SearchEdit, "search.edit", "Edit general or current-category search", VIEW, ["i"];
ViewSearch, "view.search", "Go to general YouTube Search", VIEW, ["/"];
ViewFeed, "view.feed", "Go to the combined subscription Feed", VIEW, ["F"];
ViewPrevious, "view.history.previous", "Go back in view history", VIEW, ["-", "escape"];
ViewNext, "view.history.next", "Go forward in view history", VIEW, ["+"];
ViewTabNext, "view.tab.next", "Select the next view tab", VIEW, ["tab", "ctrl+tab"];
@@ -474,7 +476,9 @@ impl Bindings {
&[
(&[VideoNext, VideoPrevious], "Select next / previous video"),
(&[VideoPlay], "Play the selected video in Terebi"),
(&[SearchEdit], "Edit the search query"),
(&[SearchEdit], "Edit general or current-category search"),
(&[ViewSearch], "Go to general YouTube Search"),
(&[ViewFeed], "Go to the combined subscription Feed"),
(
&[InfoOverview, InfoDescription],
"Info → full description → info",
+435 -93
View File
File diff suppressed because one or more lines are too long
+44 -10
View File
@@ -379,8 +379,34 @@ struct Extraction {
completed: Option<String>,
}
fn feed_commands(channels: &[String], requests: &[FeedRequest]) -> io::Result<Vec<Command>> {
fn encode_query(query: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let unreserved =
|byte: u8| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~');
let capacity = query
.bytes()
.map(|byte| if unreserved(byte) { 1 } else { 3 })
.sum();
let mut encoded = String::with_capacity(capacity);
for byte in query.bytes() {
if unreserved(byte) {
encoded.push(char::from(byte));
} else {
encoded.push('%');
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
}
encoded
}
fn feed_commands(
channels: &[String],
requests: &[FeedRequest],
query: &str,
) -> io::Result<Vec<Command>> {
validate_feed_requests(requests)?;
let query = (!query.is_empty()).then(|| encode_query(query));
let mut commands = Vec::new();
let mut bounds = None;
let batch_size = requests.len().div_ceil(MAX_EXTRACTORS);
@@ -417,10 +443,10 @@ fn feed_commands(channels: &[String], requests: &[FeedRequest]) -> io::Result<Ve
bounds = Some((request.start, request.count));
batch_len = 0;
}
commands
.last_mut()
.unwrap()
.arg(format!("{channel}/videos"));
commands.last_mut().unwrap().arg(match &query {
Some(query) => format!("{channel}/search?query={query}"),
None => format!("{channel}/videos"),
});
batch_len += 1;
}
Ok(commands)
@@ -442,8 +468,8 @@ impl Job {
]))
}
pub fn feed(channels: &[String], requests: &[FeedRequest]) -> io::Result<Self> {
Self::spawn_all(feed_commands(channels, requests)?)
pub fn feed(channels: &[String], requests: &[FeedRequest], query: &str) -> io::Result<Self> {
Self::spawn_all(feed_commands(channels, requests, query)?)
}
pub fn channel(id: &str) -> io::Result<Self> {
@@ -1583,6 +1609,14 @@ mod tests {
assert_eq!(exhausted.feed_progress[0].count, 0);
}
#[test]
fn channel_search_query_preserves_utf8_and_isolates_url_parameters() {
assert_eq!(
encode_query("café 🦀 &next=1#?/+%20\t\n\0 AZaz09-._~"),
"caf%C3%A9%20%F0%9F%A6%80%20%26next%3D1%23%3F%2F%2B%2520%09%0A%00%20AZaz09-._~"
);
}
#[test]
fn page_offsets_reject_zero_and_overflow_before_spawning() {
assert_eq!(page_end(1).unwrap(), PAGE_SIZE);
@@ -1595,12 +1629,12 @@ mod tests {
Err(error) if error.kind() == io::ErrorKind::InvalidInput
));
assert!(matches!(
Job::feed(&channels, &[FeedRequest { start, ..ONE_CHANNEL[0] }]),
Job::feed(&channels, &[FeedRequest { start, ..ONE_CHANNEL[0] }], ""),
Err(error) if error.kind() == io::ErrorKind::InvalidInput
));
}
assert!(matches!(
Job::feed(&[], &ONE_CHANNEL),
Job::feed(&[], &ONE_CHANNEL, ""),
Err(error) if error.kind() == io::ErrorKind::InvalidInput
));
}
@@ -1660,7 +1694,7 @@ mod tests {
],
] {
assert!(matches!(
Job::feed(&channels, &requests),
Job::feed(&channels, &requests, ""),
Err(error) if error.kind() == io::ErrorKind::InvalidInput
));
}