NSoiffer / MathCAT

MathCAT: Math Capable Assistive Technology for generating speech, braille, and navigation.
MIT License
61 stars 35 forks source link

MathCAT ignores preferences in a Tauri app on Android #299

Closed Almost-Senseless-Coder closed 1 week ago

Almost-Senseless-Coder commented 1 week ago

I'm using Tauri to compile an app For Windows and Android both. On Windows, MathCAT works like a charm. But on Android, I had problems:

I've verified the file structure is correct and it all works fine on Windows. I'd appreciate any pointers to further debug this problem.

NSoiffer commented 1 week ago

MathCAT reads preferences from two locations:

  1. System preferences: Rules/prefs.yaml
  2. User-specific settings: dirs::config_dir()/MathCAT/prefs.yaml

User settings take precedence over system settings. The Rust documentation mentions where dirs::config_dir() is located for Windows, MacOS, and Linux. It is silent for Android (same as Linux???). That might be one of the problems. However, if you never try to write the user file, that isn't the issue.

If you specifically set a preference, that should override any user or system setting. That might trigger rule files to be re-read though (see below for when that happens).

MathCAT has a preference that controls when the rules files are checked for changes (and potentially re-read): CheckRuleFiles. The possible values are:

I suggest trying to set the value to None and see if that helps. If that fails, try changing the value in src/prefs.rs (line 103). With value None, once the rule files are read, they won't be re-read unless you change the language, speech style, or braille code.

MathCAT is single-threaded, so there shouldn't be an issue with other threads doing the reading, but with the None setting, it cuts down on when files are read. Reading a file might allow another thread or process to run (but it won't be a MathCAT thread).

Let me know if that helps.

Almost-Senseless-Coder commented 1 week ago

I got it to work with your tip about the preference. There's a lot I don't understand yet:


#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_log::Builder::new().build())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![preview, export_html, export_epub])
        .setup(|app| {
            let setup_result = app_setup(app);
            match &setup_result {
                Ok(()) => log::info!("Completed app setup"),
                Err(e) => log::error!("Error during setup: {}", e.to_string()),
            }
            setup_result
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

fn app_setup(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
    let mathcat_rules_zip = Asset::get("MathCAT_Rules.zip").unwrap();
    let target_dir = app
        .path()
        .resolve("MathCAT_Rules", tauri::path::BaseDirectory::AppConfig)?
        .canonicalize()?;
    if target_dir.exists() && target_dir.is_dir() {
        fs::remove_dir_all(&target_dir)?;
    }
    extract(
        target_dir.parent().unwrap(),
        Cursor::new(mathcat_rules_zip.data),
    )?;
    // Originally, I configured MathCAT here, thinking the 
    // configuration would be the same during the app cycle.
    // Sadly, the configuration doesn't persist to the Tauri commands (on Android, anyway).
    // Maybe because those run on a different thread?
    Ok(())
}

/// Renders a preview version of the HTML document.
///
/// Mathematical equations get converted to MathML using KaTeX and then
/// get converted into a more accessible format using MathCAT.
#[tauri::command]
fn preview(app: AppHandle, content: &str) -> String {
    // SNIP
    let root = match create_ast(&app, content, &arena, &options, &katex_options) {
        Ok(root) => root,
        Err(e) => return e,
    };
    generate_html(root, &options)
}

fn create_ast<'a>(
    app: &AppHandle,
    content: &str,
    arena: &'a Arena<AstNode<'a>>,
    options: &Options,
    katex_options: &Opts,
) -> Result<&'a AstNode<'a>, String> {
    let root = parse_document(arena, content, options);
    let target_dir = app
        .path()
        .resolve("MathCAT_Rules", tauri::path::BaseDirectory::AppConfig)
        .unwrap()
        .canonicalize()
        .unwrap();
    // If the configuration happens **here**, it works.
    libmathcat::set_rules_dir(target_dir.to_str().unwrap().into()).unwrap();
    if libmathcat::get_preference("CheckRuleFiles".into()).unwrap() != *"None" {
        libmathcat::set_preference("CheckRuleFiles".into(), "None".into()).unwrap();
    }
    if libmathcat::get_preference("BrailleCode".into()).unwrap() != *"LaTeX" {
        libmathcat::set_preference("BrailleCode".into(), "LaTeX".into()).unwrap();
    }
    // We can't modify the document tree while traversing it, so we need to save
    // math nodes for later modification.
    let mut math_nodes = Vec::new();
    for node in root.descendants() {
        if let NodeValue::Math(_) = node.data.borrow().value {
            math_nodes.push(node);
        }
        if let NodeValue::CodeBlock(ref code) = node.data.borrow().value {
            if code.info == *"math" {
                math_nodes.push(node);
            }
        }
    }
    for node in math_nodes {
        // If, on the other hand, the setup for MathCAT happens here,
        // it doesn't work - there's an error reading files.
        if let NodeValue::Math(ref math) = node.data.borrow().value {
            if math.display_math {
                // If KaTeX doesn't parse this, just leave it unaltered.
                if let Ok(html) = katex::render_with_opts(&math.literal, katex_options) {
                    // Since we know for a fact KaTeX parsed this fine,
                    // the MathML should be valid, but it never hurts to be cautious.
                    let node_id = match libmathcat::set_mathml(extract_mathml(&html)) {
                        Ok(id) => id,
                        Err(e) => {
                            log::error!("MathCAT error: {}", libmathcat::errors_to_string(&e));
                            return Err(libmathcat::errors_to_string(&e));
                        }
                    };
                    let node_value = match libmathcat::get_braille(node_id) {
                        Ok(braille) => NodeValue::Text(braille),
                        Err(e) => {
                            log::error!("Braille error: {}", e);
                            return Err(libmathcat::errors_to_string(&e));
                        }
                    };
                    let katex_node = AstNode::from(node_value);
                    let arena_ref = arena.alloc(katex_node);
                    let para_node = AstNode::from(NodeValue::Paragraph);
                    let para_ref = arena.alloc(para_node);
                    node.insert_before(para_ref);
                    para_ref.append(arena_ref);
                    node.detach();
                }
            } else if let Ok(html) = katex::render(&math.literal) {
                let node_id = match libmathcat::set_mathml(extract_mathml(&html)) {
                    Ok(id) => id,
                    Err(e) => return Err(libmathcat::errors_to_string(&e)),
                };
                let node_value = match libmathcat::get_braille(node_id) {
                    Ok(braille) => NodeValue::Text(braille),
                    Err(e) => return Err(libmathcat::errors_to_string(&e)),
                };
                let katex_node = AstNode::from(node_value);
                let arena_ref = arena.alloc(katex_node);
                node.insert_before(arena_ref);
                node.detach();
            }
        } else if let NodeValue::CodeBlock(ref math) = node.data.borrow().value {
            if let Ok(html) = katex::render_with_opts(&math.literal, katex_options) {
                let node_id = match libmathcat::set_mathml(extract_mathml(&html)) {
                    Ok(id) => id,
                    Err(e) => return Err(libmathcat::errors_to_string(&e)),
                };
                let node_value = match libmathcat::get_braille(node_id) {
                    Ok(braille) => NodeValue::Text(braille),
                    Err(e) => return Err(libmathcat::errors_to_string(&e)),
                };
                let katex_node = AstNode::from(node_value);
                let arena_ref = arena.alloc(katex_node);
                let para_node = AstNode::from(NodeValue::Paragraph);
                let para_ref = arena.alloc(para_node);
                node.insert_before(para_ref);
                para_ref.append(arena_ref);
                node.detach();
            }
        }
    }
    Ok(root)
}

It's hard to figure out what's happening, exactly, especially because logs returned from Android are very noisy and don't include Debug messages (maybe I'll figure out sometime how I can change this). Too many moving parts at play, too: Is this a MathCAT issue, a Tauri one or an Android quirk?

Either way, it works now, so while understanding why this works but other almost identical versions don't isn't too urgent.

As a legally deafblind programmer, I owe you huge thanks for creating MathCAT. If I do manage to finish my bachelor's degree as planned, it is in part because of MathCAT - because it (finally!) made exercises and literature more accessible and (particularly) more readable to me.

Closing this as resolved, even if I don't fully understand why. :D

NSoiffer commented 1 week ago

I'm really happy to hear you managed to get the Android version to work and that MathCAT is helping you get through school. I have only met one other deafblind person. That was John Boyer, the main author for liblious. Years (decades?) ago, I did some work on braille generation for math in liblouis and it was months of corresponding with him before I found out that he was deafblind. When I finally met him in person, it was a bit surreal for me because communication still needed to take place via computer. The ability of computers to not only allow him to communicate, but to be very productive, was eye opening and remains a constant reminder to me that computers can drastically improve the lives of people with disabilities. That you managed to get MathCAT to run on Android is a testament to your abilities!

For debugging MathCAT, you need to set an environment variable when running it. I use

env RUST_LOG=DEBUG cargo run

You can set it to a few different values, but MathCAT mainly uses "debug". I'm not sure what the equivalent would be on Android (sorry, I've never done Android programming). To set it programmatically, I think you can use

env_logger::Builder::from_env(Env::default().default_filter_or("debug")).init();

Another option I found on the web (but haven't tested):

if env::var("RUST_LOG").is_err() {
    env::set_var("RUST_LOG", "debug")
}
env_logger::init();

I still don't understand why you need to set the RulesDir every time, but if things are working for you and speed is not a problem, I guess it doesn't matter.

Good luck getting your bachelor's degree!

Almost-Senseless-Coder commented 6 days ago

Thank you!

There's actually nothing about MathCAT that makes compiling it to the linux-aarch64 target difficult. Granted, incorporating it into an Android app written in Kotlin could be tricky, but I let Tauri do all the heavy lifting.

You said MathCAT is single-threaded. So unless I'm misinterpreting things, my problems were partly caused by multiple threads or possibly async tasks. I.e., MathCAT gets initialized on one thread, but the subsequent call to set_mathml happens on a different thread due to how Tauri apps run. That's my only explanation for the behaviour I see, honestly; can't reproduce it on Windows. Would there be a way to share a MathCAT instance between threads? Tauri has shared state, but I honestly don't know how I'd go about putting a MathCAT instance in such a shared state...

One optimization I can make is to check if there are any MathNodes in the AST at all - if not, MathCAT won't get needed so I don't need to call initialization again. Overall, though, the performance seems to be good, plus there's already a debouncing mechanism in place to keep preview rerendering to an acceptable level.

There's a lot more I'd like to know and share, but I don't want to spam your GitHub issues and it's not technically an issue with MathCAT. So if you'd like, you can reach me at contact[at]tim-boettcher.email.

NSoiffer commented 6 days ago

MathCAT has state (bad for multi-threading), but I'm pretty sure that by being single-threaded (only one thread/instance of MathCAT can exist), any calls to MathCAT will be shared, one at a time.

My hypothesis is that you are actually starting a new MathCAT each call and that is why the Rules directory needs to be set each time. One possible way to detect this is to do a time measurement. MathCAT on my 7 year old Windows machine takes about 50ms to read in all the rules and produce speech and braille the first time, but after that, it only takes 4ms or less. So if you time the calls and don't see it taking substantially less on a subsequent call, that is a strong sign that in fact you are not reusing the MathCAT instance.

I'll contact you at your email addr so I can answer other technical issues you have.