Some fixes
This commit is contained in:
parent
50f8b8cfba
commit
e6c694a740
12 changed files with 230 additions and 67 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1 +1 @@
|
||||||
target
|
/target
|
||||||
|
|
|
||||||
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -713,12 +713,12 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"encoding_rs",
|
|
||||||
"lofty",
|
"lofty",
|
||||||
"nucleo-matcher",
|
"nucleo-matcher",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"textfix",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ureq",
|
"ureq",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
|
@ -1031,11 +1031,13 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cpal",
|
"cpal",
|
||||||
|
"encoding_rs",
|
||||||
"rand",
|
"rand",
|
||||||
"realfft",
|
"realfft",
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
"rubato",
|
"rubato",
|
||||||
"symphonia",
|
"symphonia",
|
||||||
|
"textfix",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
@ -1711,6 +1713,13 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "textfix"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ members = [
|
||||||
"crates/library",
|
"crates/library",
|
||||||
"crates/tui",
|
"crates/tui",
|
||||||
"crates/nsm",
|
"crates/nsm",
|
||||||
|
"crates/textfix",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
|
||||||
23
README.en.md
23
README.en.md
|
|
@ -65,6 +65,29 @@ cargo build --release
|
||||||
|
|
||||||
The binary will be at `target/release/nsm` (on Windows — `target\release\nsm.exe`).
|
The binary will be at `target/release/nsm` (on Windows — `target\release\nsm.exe`).
|
||||||
|
|
||||||
|
### Cross-compiling a Windows binary from Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Add the Windows target (needs rustup — the normal Rust install via rustup.rs)
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# 2. Install mingw-w64 — the Windows cross-compiler/linker
|
||||||
|
sudo apt-get install gcc-mingw-w64-x86-64 # Debian/Ubuntu; use your own package manager otherwise
|
||||||
|
|
||||||
|
# 3. Tell cargo which linker to use for this target
|
||||||
|
mkdir -p .cargo
|
||||||
|
cat >> .cargo/config.toml <<EOF
|
||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
linker = "x86_64-w64-mingw32-gcc"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. Build
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
The binary will be at `target/x86_64-pc-windows-gnu/release/nsm.exe`.
|
||||||
|
|
||||||
|
**Partially verified**: the linker itself (`mingw-w64`) was actually tested — compiled a test C program with it and got a genuine Windows PE executable. Couldn't verify a full end-to-end `nsm` build for the Windows target (that needs `rustup` to fetch the Windows std library, which this environment can't reach) — but along the way found and fixed a real bug: the code used a Unix-specific way to detect the terminal's cell size for cover art, which would have made the Windows build fail to compile at all. Non-Unix platforms now use a sensible default instead.
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
23
README.md
23
README.md
|
|
@ -78,6 +78,29 @@ cargo build --release
|
||||||
|
|
||||||
Бинарник появится в `target/release/nsm` (на Windows — `target\release\nsm.exe`).
|
Бинарник появится в `target/release/nsm` (на Windows — `target\release\nsm.exe`).
|
||||||
|
|
||||||
|
### Сборка Windows-бинарника с Linux (кросс-компиляция)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Добавить Windows-таргет (нужен rustup — обычная установка Rust с rustup.rs)
|
||||||
|
rustup target add x86_64-pc-windows-gnu
|
||||||
|
|
||||||
|
# 2. Поставить mingw-w64 — кросс-компилятор/линкер под Windows
|
||||||
|
sudo apt-get install gcc-mingw-w64-x86-64 # Debian/Ubuntu; на других — свой пакетный менеджер
|
||||||
|
|
||||||
|
# 3. Указать cargo, каким линкером пользоваться для этого таргета
|
||||||
|
mkdir -p .cargo
|
||||||
|
cat >> .cargo/config.toml <<EOF
|
||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
linker = "x86_64-w64-mingw32-gcc"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4. Собрать
|
||||||
|
cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
Бинарник появится в `target/x86_64-pc-windows-gnu/release/nsm.exe`.
|
||||||
|
|
||||||
|
**Проверено частично**: сам линкер (`mingw-w64`) реально протестирован — собрал им тестовую C-программу, получился настоящий Windows PE-исполняемый файл. Полную сборку `nsm` под Windows-таргет от начала до конца проверить не удалось (нужен `rustup` для загрузки Windows std-библиотеки, а в этом окружении к нему нет доступа) — но по ходу проверки нашёлся и уже исправлен реальный баг: код использовал Unix-специфичный способ определения размера ячейки терминала для обложки, из-за чего сборка под Windows не прошла бы вообще. Теперь на не-Unix платформах используется разумное значение по умолчанию.
|
||||||
|
|
||||||
### Запуск
|
### Запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ lofty = "0.19"
|
||||||
nucleo-matcher = "0.3"
|
nucleo-matcher = "0.3"
|
||||||
ureq = { version = "2", features = ["json"] }
|
ureq = { version = "2", features = ["json"] }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
|
||||||
encoding_rs = "0.8"
|
textfix = { path = "../textfix" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::cue;
|
use crate::cue;
|
||||||
use crate::model::{is_audio_file, is_cue_file, Track};
|
use crate::model::{is_audio_file, is_cue_file, Track};
|
||||||
|
use textfix::fix_mojibake;
|
||||||
|
|
||||||
/// Рекурсивно обходит `root` и все вложенные папки. Сначала обрабатывает `.cue`-листы —
|
/// Рекурсивно обходит `root` и все вложенные папки. Сначала обрабатывает `.cue`-листы —
|
||||||
/// каждый превращается в несколько "виртуальных" треков (диапазонов внутри одного
|
/// каждый превращается в несколько "виртуальных" треков (диапазонов внутри одного
|
||||||
|
|
@ -172,47 +173,8 @@ fn parse_replay_gain_db(raw: &str) -> Option<f32> {
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Чинит "кракозябры" — частый случай со старыми (обычно ID3v1) тегами, где кириллица
|
// fix_mojibake переехала в общий крейт `textfix`, используется и здесь, и в player::decode —
|
||||||
/// была записана в CP1251, а прочитана так, будто это Latin-1/ISO-8859-1 (типичное
|
// раньше была только тут, из-за чего кракозябры чинились в библиотеке, но не в Now Playing.
|
||||||
/// поведение множества старых тегеров и библиотек). Раз символы результата — это
|
|
||||||
/// однозначно code points ≤0xFF (свойство Latin-1-декодирования), исходные байты
|
|
||||||
/// восстанавливаются 1:1, и остаётся просто раскодировать их как CP1251. Если результат
|
|
||||||
/// не выглядит правдоподобно кириллическим — возвращаем строку как есть, не трогаем.
|
|
||||||
fn fix_mojibake(s: &str) -> String {
|
|
||||||
if s.is_ascii() {
|
|
||||||
return s.to_string();
|
|
||||||
}
|
|
||||||
let bytes: Option<Vec<u8>> = s
|
|
||||||
.chars()
|
|
||||||
.map(|c| {
|
|
||||||
let cp = c as u32;
|
|
||||||
if cp <= 0xFF {
|
|
||||||
Some(cp as u8)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let Some(bytes) = bytes else {
|
|
||||||
return s.to_string(); // содержит символы вне Latin-1 — это не наш случай, не трогаем
|
|
||||||
};
|
|
||||||
|
|
||||||
let (decoded, _, had_errors) = encoding_rs::WINDOWS_1251.decode(&bytes);
|
|
||||||
if had_errors {
|
|
||||||
return s.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let cyrillic_count = decoded
|
|
||||||
.chars()
|
|
||||||
.filter(|c| ('А'..='я').contains(c) || *c == 'Ё' || *c == 'ё')
|
|
||||||
.count();
|
|
||||||
let total_alpha = decoded.chars().filter(|c| c.is_alphabetic()).count().max(1);
|
|
||||||
if cyrillic_count * 2 >= total_alpha {
|
|
||||||
decoded.into_owned()
|
|
||||||
} else {
|
|
||||||
s.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Достаёт обложку альбома (первую найденную картинку) для конкретного файла.
|
/// Достаёт обложку альбома (первую найденную картинку) для конкретного файла.
|
||||||
/// Читается по требованию (не при сканировании), чтобы не раздувать память на всю библиотеку.
|
/// Читается по требованию (не при сканировании), чтобы не раздувать память на всю библиотеку.
|
||||||
|
|
@ -241,23 +203,5 @@ mod tests {
|
||||||
fn returns_none_for_garbage() {
|
fn returns_none_for_garbage() {
|
||||||
assert_eq!(parse_replay_gain_db("not a number"), None);
|
assert_eq!(parse_replay_gain_db("not a number"), None);
|
||||||
}
|
}
|
||||||
|
// fix_mojibake теперь тестируется в крейте textfix, где она и живёт.
|
||||||
#[test]
|
|
||||||
fn fixes_cp1251_mojibake() {
|
|
||||||
// "Ïåñíÿ" — это ровно то, что получается, когда CP1251-байты слова "Песня"
|
|
||||||
// прочитаны как Latin-1 (типичное поведение старых ID3v1-тегов).
|
|
||||||
assert_eq!(fix_mojibake("Ïåñíÿ"), "Песня");
|
|
||||||
assert_eq!(fix_mojibake("Àðòèñò"), "Артист");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn leaves_plain_ascii_untouched() {
|
|
||||||
assert_eq!(fix_mojibake("Hello World"), "Hello World");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn leaves_already_correct_cyrillic_untouched() {
|
|
||||||
// уже нормальный UTF-8 текст не должен портиться повторной перекодировкой
|
|
||||||
assert_eq!(fix_mojibake("Пример"), "Пример");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,7 @@ rand = "0.8"
|
||||||
ringbuf = "0.3"
|
ringbuf = "0.3"
|
||||||
ureq = "2"
|
ureq = "2"
|
||||||
realfft = "3"
|
realfft = "3"
|
||||||
|
textfix = { path = "../textfix" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
encoding_rs = "0.8"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use symphonia::core::probe::Hint;
|
||||||
use symphonia::core::units::Time;
|
use symphonia::core::units::Time;
|
||||||
|
|
||||||
use crate::types::{PlayerEvent, TrackInfo};
|
use crate::types::{PlayerEvent, TrackInfo};
|
||||||
|
use textfix::fix_mojibake;
|
||||||
|
|
||||||
pub struct DecodedTrack {
|
pub struct DecodedTrack {
|
||||||
pub format: Box<dyn FormatReader>,
|
pub format: Box<dyn FormatReader>,
|
||||||
|
|
@ -313,9 +314,9 @@ pub fn read_tags(path: &Path) -> anyhow::Result<TrackInfo> {
|
||||||
|
|
||||||
Ok(TrackInfo {
|
Ok(TrackInfo {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
title: title.unwrap_or(fallback_title),
|
title: fix_mojibake(&title.unwrap_or(fallback_title)),
|
||||||
artist: artist.unwrap_or_else(|| "Unknown Artist".to_string()),
|
artist: fix_mojibake(&artist.unwrap_or_else(|| "Unknown Artist".to_string())),
|
||||||
album: album.unwrap_or_else(|| "Unknown Album".to_string()),
|
album: fix_mojibake(&album.unwrap_or_else(|| "Unknown Album".to_string())),
|
||||||
duration,
|
duration,
|
||||||
cover,
|
cover,
|
||||||
})
|
})
|
||||||
|
|
@ -346,6 +347,73 @@ mod tests {
|
||||||
assert_eq!(parse_stream_title(meta), None);
|
assert_eq!(parse_stream_title(meta), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Регрессионный тест на реальный баг: кракозябры чинились в библиотеке (library::scanner),
|
||||||
|
/// но не в TrackInfo, который показывается в "Now Playing" при реальном воспроизведении —
|
||||||
|
/// потому что read_tags жила отдельно и не применяла fix_mojibake. Воспроизводит точный
|
||||||
|
/// сценарий из багрепорта: ID3v2-тег, где байты CP1251 записаны под флагом ISO-8859-1
|
||||||
|
/// (классический баг старых тегеров на русских Windows) — именно так, а не ID3v1
|
||||||
|
/// (symphonia, в отличие от lofty, ID3v1 не читает вовсе). Нужен ffmpeg — запускать
|
||||||
|
/// явно: `cargo test -- --ignored`.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn read_tags_fixes_cp1251_mojibake_for_now_playing() {
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let plain = dir.join("nsm_moji_plain.mp3");
|
||||||
|
let tagged = dir.join("nsm_moji_tagged.mp3");
|
||||||
|
|
||||||
|
let status = std::process::Command::new("ffmpeg")
|
||||||
|
.args([
|
||||||
|
"-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=1",
|
||||||
|
"-write_id3v1", "0", "-id3v2_version", "0",
|
||||||
|
plain.to_str().unwrap(), "-loglevel", "error",
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.expect("ffmpeg must be installed for this test");
|
||||||
|
assert!(status.success());
|
||||||
|
|
||||||
|
// ID3v2.3 фрейм с encoding byte = 0 (ISO-8859-1), но реальные байты — CP1251.
|
||||||
|
// Именно так старые тегеры на русской Windows пишут кириллицу, "забывая" указать
|
||||||
|
// правильную кодировку.
|
||||||
|
fn frame(id: &str, text_cp1251: &[u8]) -> Vec<u8> {
|
||||||
|
let mut payload = vec![0u8]; // encoding = 0 (ISO-8859-1)
|
||||||
|
payload.extend_from_slice(text_cp1251);
|
||||||
|
let mut out = id.as_bytes().to_vec();
|
||||||
|
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(&[0, 0]); // flags
|
||||||
|
out.extend_from_slice(&payload);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
fn synchsafe(n: u32) -> [u8; 4] {
|
||||||
|
[
|
||||||
|
((n >> 21) & 0x7f) as u8,
|
||||||
|
((n >> 14) & 0x7f) as u8,
|
||||||
|
((n >> 7) & 0x7f) as u8,
|
||||||
|
(n & 0x7f) as u8,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
let (title_bytes, _, _) = encoding_rs::WINDOWS_1251.encode("Поговори с ней о сексе");
|
||||||
|
let (artist_bytes, _, _) = encoding_rs::WINDOWS_1251.encode("Мальчишник");
|
||||||
|
let mut frames = frame("TIT2", &title_bytes);
|
||||||
|
frames.extend(frame("TPE1", &artist_bytes));
|
||||||
|
|
||||||
|
let mut header = vec![b'I', b'D', b'3', 3, 0, 0];
|
||||||
|
header.extend_from_slice(&synchsafe(frames.len() as u32));
|
||||||
|
|
||||||
|
let audio = std::fs::read(&plain).unwrap();
|
||||||
|
let mut data = header;
|
||||||
|
data.extend(frames);
|
||||||
|
data.extend(audio);
|
||||||
|
std::fs::write(&tagged, &data).unwrap();
|
||||||
|
|
||||||
|
let info = read_tags(&tagged).expect("should read tags");
|
||||||
|
assert_eq!(info.title, "Поговори с ней о сексе");
|
||||||
|
assert_eq!(info.artist, "Мальчишник");
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&plain);
|
||||||
|
let _ = std::fs::remove_file(&tagged);
|
||||||
|
}
|
||||||
|
|
||||||
/// Ручная проверка реального seek на настоящем файле. Игнорируется по умолчанию
|
/// Ручная проверка реального seek на настоящем файле. Игнорируется по умолчанию
|
||||||
/// (нужен ffmpeg в PATH для генерации фикстуры) — запускать явно: `cargo test -- --ignored`.
|
/// (нужен ffmpeg в PATH для генерации фикстуры) — запускать явно: `cargo test -- --ignored`.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
7
crates/textfix/Cargo.toml
Normal file
7
crates/textfix/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "textfix"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
encoding_rs = "0.8"
|
||||||
77
crates/textfix/src/lib.rs
Normal file
77
crates/textfix/src/lib.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
//! Исправление "кракозябр" в тегах — общая логика для `library` (сканирование библиотеки)
|
||||||
|
//! и `player` (чтение тегов текущего трека для "Now Playing"). Раньше жила только в одном
|
||||||
|
//! из двух мест, из-за чего кракозябры чинились в списке библиотеки, но не в панели
|
||||||
|
//! Now Playing при реальном воспроизведении — вынесено сюда, чтобы такого больше не было.
|
||||||
|
|
||||||
|
/// Чинит частый случай со старыми (обычно ID3v1, но встречается и в ID3v2) тегами, где
|
||||||
|
/// кириллица была записана в CP1251, а прочитана так, будто это Latin-1/ISO-8859-1
|
||||||
|
/// (типичное поведение множества старых тегеров и библиотек метаданных). Раз символы
|
||||||
|
/// результата — это однозначно code points ≤0xFF (свойство Latin-1-декодирования),
|
||||||
|
/// исходные байты восстанавливаются 1:1, и остаётся просто раскодировать их как CP1251.
|
||||||
|
/// Если результат не выглядит правдоподобно кириллическим — возвращаем строку как есть.
|
||||||
|
pub fn fix_mojibake(s: &str) -> String {
|
||||||
|
if s.is_ascii() {
|
||||||
|
return s.to_string();
|
||||||
|
}
|
||||||
|
let bytes: Option<Vec<u8>> = s
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
let cp = c as u32;
|
||||||
|
if cp <= 0xFF {
|
||||||
|
Some(cp as u8)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let Some(bytes) = bytes else {
|
||||||
|
return s.to_string(); // содержит символы вне Latin-1 — это не наш случай, не трогаем
|
||||||
|
};
|
||||||
|
|
||||||
|
let (decoded, _, had_errors) = encoding_rs::WINDOWS_1251.decode(&bytes);
|
||||||
|
if had_errors {
|
||||||
|
return s.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let cyrillic_count = decoded
|
||||||
|
.chars()
|
||||||
|
.filter(|c| ('А'..='я').contains(c) || *c == 'Ё' || *c == 'ё')
|
||||||
|
.count();
|
||||||
|
let total_alpha = decoded.chars().filter(|c| c.is_alphabetic()).count().max(1);
|
||||||
|
if cyrillic_count * 2 >= total_alpha {
|
||||||
|
decoded.into_owned()
|
||||||
|
} else {
|
||||||
|
s.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixes_cp1251_mojibake() {
|
||||||
|
// "Ïåñíÿ" — это ровно то, что получается, когда CP1251-байты слова "Песня"
|
||||||
|
// прочитаны как Latin-1 (типичное поведение старых ID3v1-тегов).
|
||||||
|
assert_eq!(fix_mojibake("Ïåñíÿ"), "Песня");
|
||||||
|
assert_eq!(fix_mojibake("Àðòèñò"), "Артист");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixes_real_world_example_from_bug_report() {
|
||||||
|
// Реальный случай из багрепорта: старый mp3, поймано в Now Playing, но не
|
||||||
|
// в библиотеке — то есть именно то расхождение, ради которого этот крейт создан.
|
||||||
|
assert_eq!(fix_mojibake("Ìàëü÷èøíèê"), "Мальчишник");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaves_plain_ascii_untouched() {
|
||||||
|
assert_eq!(fix_mojibake("Hello World"), "Hello World");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaves_already_correct_cyrillic_untouched() {
|
||||||
|
// уже нормальный UTF-8 текст не должен портиться повторной перекодировкой
|
||||||
|
assert_eq!(fix_mojibake("Пример"), "Пример");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -271,7 +271,16 @@ impl App {
|
||||||
&library::default_new_releases_cache_path(),
|
&library::default_new_releases_cache_path(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let picker = Picker::from_termios().ok().map(|mut p| {
|
// `Picker::from_termios()` доступен только на Unix (использует termios-иоктлы для
|
||||||
|
// определения размера ячейки терминала в пикселях) — на Windows такого API в этом
|
||||||
|
// крейте нет, используем разумный дефолт (8x16 — типичный размер моноширинной
|
||||||
|
// ячейки), протокол всё равно определяется через guess_protocol() кросс-платформенно.
|
||||||
|
#[cfg(unix)]
|
||||||
|
let base_picker = Picker::from_termios().ok();
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let base_picker = Some(Picker::new((8, 16)));
|
||||||
|
|
||||||
|
let picker = base_picker.map(|mut p| {
|
||||||
p.guess_protocol();
|
p.guess_protocol();
|
||||||
if let Some(saved) = settings.cover_protocol.as_deref().and_then(protocol_from_str) {
|
if let Some(saved) = settings.cover_protocol.as_deref().and_then(protocol_from_str) {
|
||||||
p.protocol_type = saved;
|
p.protocol_type = saved;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue