aboutsummaryrefslogtreecommitdiff
path: root/bundle/src-tauri/src/main.rs
blob: c9babb401fb6187b3a65e0b0796de3c45f363df4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod compiler;
mod menu;
mod runner;
mod update;
use compiler::Compiler;
use runner::{OutputEvent, Runner};
use std::{
    collections::HashMap,
    sync::{
        atomic::{AtomicUsize, Ordering},
        Mutex,
    },
};
use tauri::{
    ipc::Channel, AppHandle, Emitter, Manager, State, WebviewWindow, WebviewWindowBuilder,
};

// Each window runs its own program, keyed by window label.
#[derive(Default)]
struct Runners(Mutex<HashMap<String, Runner>>);

impl Runners {
    fn get(&self, label: &str) -> Runner {
        self.0
            .lock()
            .unwrap()
            .entry(label.into())
            .or_default()
            .clone()
    }
}

#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(path).map_err(|error| error.to_string())
}

#[tauri::command]
fn write_file(path: String, text: String) -> Result<(), String> {
    std::fs::write(path, text).map_err(|error| error.to_string())
}

#[tauri::command]
async fn run_file(
    path: String,
    input: String,
    output: Channel<OutputEvent>,
    window: WebviewWindow,
    runners: State<'_, Runners>,
    compiler: State<'_, Compiler>,
) -> Result<(), String> {
    let executable = compiler.inner().0.clone()?;
    let result = runners
        .get(window.label())
        .run(executable, path.into(), input, |stream, bytes| {
            let _ = output.send(OutputEvent::Data {
                stream: stream.into(),
                bytes,
            });
        })
        .await?;
    output
        .send(OutputEvent::Finished {
            code: result.code,
            stopped: result.stopped,
        })
        .map_err(|error| error.to_string())
}

#[tauri::command]
async fn stop_run(window: WebviewWindow, runners: State<'_, Runners>) -> Result<(), String> {
    runners.get(window.label()).stop().await;
    Ok(())
}

// Async because creating a window from a synchronous command deadlocks on Windows.
#[tauri::command]
async fn new_window(app: AppHandle) -> Result<(), String> {
    static NEXT: AtomicUsize = AtomicUsize::new(1);
    let mut config = app.config().app.windows[0].clone();
    config.label = format!("window-{}", NEXT.fetch_add(1, Ordering::Relaxed));
    WebviewWindowBuilder::from_config(&app, &config)
        .and_then(|builder| builder.build())
        .map(drop)
        .map_err(|error| error.to_string())
}

fn main() {
    let compiler = Compiler::load();
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_opener::init())
        // Once per launch, not per window.
        .setup(|app| {
            update::check(app.handle().clone());
            Ok(())
        })
        .menu(menu::build)
        // Menu items act on the focused window, like its keyboard shortcuts.
        .on_menu_event(|app, event| {
            let mut windows = app.webview_windows().into_values();
            if event.id() == "quit" {
                // Each window saves changes before closing; the app exits after the last.
                windows.for_each(|window| drop(window.close()));
            } else if let Some(window) = windows.find(|window| window.is_focused().unwrap_or(false))
            {
                let _ = window.emit_to(window.label(), "menu", event.id().as_ref());
            }
        })
        .manage(Runners::default())
        .manage(compiler)
        .invoke_handler(tauri::generate_handler![
            read_file, write_file, run_file, stop_run, new_window
        ])
        .build(tauri::generate_context!())
        .expect("Could not start CPSC Bundle")
        .run(|app, event| match event {
            // Dock Quit must run the same document-close flow as the menu and title bar.
            tauri::RunEvent::ExitRequested { api, code: None, .. } => {
                let windows = app.webview_windows();
                if !windows.is_empty() {
                    api.prevent_exit();
                    for window in windows.into_values() {
                        let _ = window.close();
                    }
                }
            }
            tauri::RunEvent::Exit => {
                let runners: Vec<Runner> = app
                    .state::<Runners>()
                    .0
                    .lock()
                    .unwrap()
                    .values()
                    .cloned()
                    .collect();
                tauri::async_runtime::block_on(async {
                    for runner in runners {
                        runner.stop().await;
                    }
                });
            }
            tauri::RunEvent::WindowEvent {
                label,
                event: tauri::WindowEvent::Destroyed,
                ..
            } => {
                let runner = app.state::<Runners>().0.lock().unwrap().remove(&label);
                if let Some(runner) = runner {
                    tauri::async_runtime::block_on(runner.stop());
                }
            }
            _ => {}
        });
}