Skip to content

Commit 1a2c187

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat(gui): implement FD-M07 basic Tauri GUI shell
- Add Tauri 2.0 app with plugins (dialog, fs, shell) - Create IPC commands for all 7 format conversions - Add document event tracking (FD-M12 partial) - Configure window settings and icons - ReScript UI scaffold already exists in ui/ Build requires toolbox with: glib2-devel gtk3-devel webkit2gtk4.1-devel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 169ad6f commit 1a2c187

12 files changed

Lines changed: 952 additions & 8 deletions

File tree

Cargo.lock

Lines changed: 599 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/formatrix-gui/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gen/

crates/formatrix-gui/Cargo.toml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,34 @@ authors.workspace = true
88
license.workspace = true
99
repository.workspace = true
1010

11+
[lib]
12+
name = "formatrix_gui_lib"
13+
crate-type = ["staticlib", "cdylib", "rlib"]
14+
15+
[[bin]]
16+
name = "formatrix-gui"
17+
path = "src/main.rs"
18+
1119
[build-dependencies]
12-
tauri-build.workspace = true
20+
tauri-build = { workspace = true, features = [] }
1321

1422
[dependencies]
1523
formatrix-core = { path = "../formatrix-core" }
16-
formatrix-db = { path = "../formatrix-db" }
17-
formatrix-pipeline = { path = "../formatrix-pipeline" }
24+
1825
tauri.workspace = true
26+
tauri-plugin-dialog = "2"
27+
tauri-plugin-fs = "2"
28+
tauri-plugin-shell = "2"
29+
1930
serde.workspace = true
2031
serde_json.workspace = true
32+
thiserror.workspace = true
2133
tokio.workspace = true
2234
tracing.workspace = true
35+
tracing-subscriber.workspace = true
36+
37+
# Hashing for document events
38+
sha2 = "0.10"
2339

2440
[features]
2541
default = ["custom-protocol"]

crates/formatrix-gui/build.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//! Tauri build script for formatrix-gui
3+
4+
fn main() {
5+
tauri_build::build()
6+
}
1.36 KB
Loading
2.4 KB
Loading
592 Bytes
Loading
1.36 KB
Binary file not shown.
4.19 KB
Binary file not shown.

crates/formatrix-gui/src/commands.rs

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
22
//! Tauri commands for document operations
33
4-
use formatrix_core::{Document, ParseConfig, RenderConfig, SourceFormat};
4+
use formatrix_core::{ParseConfig, RenderConfig};
55
use serde::{Deserialize, Serialize};
66
use sha2::{Sha256, Digest};
77

@@ -243,7 +243,10 @@ pub async fn convert_to_format(
243243
from_format: String,
244244
to_format: String,
245245
) -> Result<ConversionResult, String> {
246-
use formatrix_core::formats::{MarkdownHandler, PlainTextHandler};
246+
use formatrix_core::formats::{
247+
AsciidocHandler, DjotHandler, MarkdownHandler, OrgModeHandler, PlainTextHandler,
248+
RstHandler, TypstHandler,
249+
};
247250
use formatrix_core::traits::{Parser, Renderer};
248251

249252
// For now, just return the content as-is if converting to same format
@@ -265,6 +268,21 @@ pub async fn convert_to_format(
265268
"md" => MarkdownHandler::new()
266269
.parse(&content, &parse_config)
267270
.map_err(|e| e.to_string())?,
271+
"adoc" => AsciidocHandler::new()
272+
.parse(&content, &parse_config)
273+
.map_err(|e| e.to_string())?,
274+
"djot" => DjotHandler::new()
275+
.parse(&content, &parse_config)
276+
.map_err(|e| e.to_string())?,
277+
"org" => OrgModeHandler::new()
278+
.parse(&content, &parse_config)
279+
.map_err(|e| e.to_string())?,
280+
"rst" => RstHandler::new()
281+
.parse(&content, &parse_config)
282+
.map_err(|e| e.to_string())?,
283+
"typ" => TypstHandler::new()
284+
.parse(&content, &parse_config)
285+
.map_err(|e| e.to_string())?,
268286
_ => {
269287
return Err(format!("Unsupported source format: {}", from_format));
270288
}
@@ -278,13 +296,190 @@ pub async fn convert_to_format(
278296
"md" => MarkdownHandler::new()
279297
.render(&doc, &render_config)
280298
.map_err(|e| e.to_string())?,
299+
"adoc" => AsciidocHandler::new()
300+
.render(&doc, &render_config)
301+
.map_err(|e| e.to_string())?,
302+
"djot" => DjotHandler::new()
303+
.render(&doc, &render_config)
304+
.map_err(|e| e.to_string())?,
305+
"org" => OrgModeHandler::new()
306+
.render(&doc, &render_config)
307+
.map_err(|e| e.to_string())?,
308+
"rst" => RstHandler::new()
309+
.render(&doc, &render_config)
310+
.map_err(|e| e.to_string())?,
311+
"typ" => TypstHandler::new()
312+
.render(&doc, &render_config)
313+
.map_err(|e| e.to_string())?,
281314
_ => {
282315
return Err(format!("Unsupported target format: {}", to_format));
283316
}
284317
};
285318

319+
// Emit conversion event
320+
emit_event(DocumentEvent::converted(&content, &output, &from_format, &to_format));
321+
286322
Ok(ConversionResult {
287323
content: output,
288324
warnings: Vec::new(),
289325
})
290326
}
327+
328+
/// Parsed document result for frontend
329+
#[derive(Debug, Clone, Serialize, Deserialize)]
330+
pub struct ParsedDocument {
331+
pub title: Option<String>,
332+
pub block_count: usize,
333+
pub format: String,
334+
}
335+
336+
/// Parse a document and return metadata
337+
#[tauri::command]
338+
pub async fn parse_document(content: String, format: String) -> Result<ParsedDocument, String> {
339+
use formatrix_core::formats::{
340+
AsciidocHandler, DjotHandler, MarkdownHandler, OrgModeHandler, PlainTextHandler,
341+
RstHandler, TypstHandler,
342+
};
343+
use formatrix_core::traits::Parser;
344+
345+
let parse_config = ParseConfig::default();
346+
347+
let doc = match format.as_str() {
348+
"txt" => PlainTextHandler::new()
349+
.parse(&content, &parse_config)
350+
.map_err(|e| e.to_string())?,
351+
"md" => MarkdownHandler::new()
352+
.parse(&content, &parse_config)
353+
.map_err(|e| e.to_string())?,
354+
"adoc" => AsciidocHandler::new()
355+
.parse(&content, &parse_config)
356+
.map_err(|e| e.to_string())?,
357+
"djot" => DjotHandler::new()
358+
.parse(&content, &parse_config)
359+
.map_err(|e| e.to_string())?,
360+
"org" => OrgModeHandler::new()
361+
.parse(&content, &parse_config)
362+
.map_err(|e| e.to_string())?,
363+
"rst" => RstHandler::new()
364+
.parse(&content, &parse_config)
365+
.map_err(|e| e.to_string())?,
366+
"typ" => TypstHandler::new()
367+
.parse(&content, &parse_config)
368+
.map_err(|e| e.to_string())?,
369+
_ => {
370+
return Err(format!("Unsupported format: {}", format));
371+
}
372+
};
373+
374+
Ok(ParsedDocument {
375+
title: doc.meta.title,
376+
block_count: doc.content.len(),
377+
format,
378+
})
379+
}
380+
381+
/// Render a document from AST JSON (for advanced use)
382+
#[tauri::command]
383+
pub async fn render_document(content: String, to_format: String) -> Result<String, String> {
384+
use formatrix_core::formats::{
385+
AsciidocHandler, DjotHandler, MarkdownHandler, OrgModeHandler, PlainTextHandler,
386+
RstHandler, TypstHandler,
387+
};
388+
use formatrix_core::traits::{Parser, Renderer};
389+
390+
// Parse as markdown by default for rendering
391+
let parse_config = ParseConfig::default();
392+
let render_config = RenderConfig::default();
393+
394+
let doc = MarkdownHandler::new()
395+
.parse(&content, &parse_config)
396+
.map_err(|e| e.to_string())?;
397+
398+
let output = match to_format.as_str() {
399+
"txt" => PlainTextHandler::new()
400+
.render(&doc, &render_config)
401+
.map_err(|e| e.to_string())?,
402+
"md" => MarkdownHandler::new()
403+
.render(&doc, &render_config)
404+
.map_err(|e| e.to_string())?,
405+
"adoc" => AsciidocHandler::new()
406+
.render(&doc, &render_config)
407+
.map_err(|e| e.to_string())?,
408+
"djot" => DjotHandler::new()
409+
.render(&doc, &render_config)
410+
.map_err(|e| e.to_string())?,
411+
"org" => OrgModeHandler::new()
412+
.render(&doc, &render_config)
413+
.map_err(|e| e.to_string())?,
414+
"rst" => RstHandler::new()
415+
.render(&doc, &render_config)
416+
.map_err(|e| e.to_string())?,
417+
"typ" => TypstHandler::new()
418+
.render(&doc, &render_config)
419+
.map_err(|e| e.to_string())?,
420+
_ => {
421+
return Err(format!("Unsupported target format: {}", to_format));
422+
}
423+
};
424+
425+
Ok(output)
426+
}
427+
428+
/// Detect format from content using heuristics
429+
#[tauri::command]
430+
pub fn detect_format(content: String) -> String {
431+
use formatrix_core::file_ops::format_from_content;
432+
433+
let format = format_from_content(&content);
434+
format.extension().to_string()
435+
}
436+
437+
/// Format info for frontend
438+
#[derive(Debug, Clone, Serialize, Deserialize)]
439+
pub struct FormatInfo {
440+
pub id: String,
441+
pub label: String,
442+
pub extension: String,
443+
}
444+
445+
/// Get list of supported formats
446+
#[tauri::command]
447+
pub fn get_supported_formats() -> Vec<FormatInfo> {
448+
vec![
449+
FormatInfo {
450+
id: "txt".to_string(),
451+
label: "Plain Text".to_string(),
452+
extension: "txt".to_string(),
453+
},
454+
FormatInfo {
455+
id: "md".to_string(),
456+
label: "Markdown".to_string(),
457+
extension: "md".to_string(),
458+
},
459+
FormatInfo {
460+
id: "adoc".to_string(),
461+
label: "AsciiDoc".to_string(),
462+
extension: "adoc".to_string(),
463+
},
464+
FormatInfo {
465+
id: "djot".to_string(),
466+
label: "Djot".to_string(),
467+
extension: "dj".to_string(),
468+
},
469+
FormatInfo {
470+
id: "org".to_string(),
471+
label: "Org-mode".to_string(),
472+
extension: "org".to_string(),
473+
},
474+
FormatInfo {
475+
id: "rst".to_string(),
476+
label: "reStructuredText".to_string(),
477+
extension: "rst".to_string(),
478+
},
479+
FormatInfo {
480+
id: "typ".to_string(),
481+
label: "Typst".to_string(),
482+
extension: "typ".to_string(),
483+
},
484+
]
485+
}

0 commit comments

Comments
 (0)