Initial Libre Study app scaffold
This commit is contained in:
@@ -0,0 +1,563 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_library_state,
|
||||
search_verses,
|
||||
add_tag_to_verse
|
||||
])
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LibraryState {
|
||||
verses: Vec<Verse>,
|
||||
tags: Vec<Tag>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Verse {
|
||||
id: i64,
|
||||
book: String,
|
||||
chapter: i64,
|
||||
verse: i64,
|
||||
text: String,
|
||||
strongs: Vec<StrongLink>,
|
||||
tags: Vec<Tag>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StrongLink {
|
||||
strong: String,
|
||||
lemma: String,
|
||||
transliteration: String,
|
||||
definition: String,
|
||||
language: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Tag {
|
||||
id: i64,
|
||||
label: String,
|
||||
color: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ImportedVerse {
|
||||
id: String,
|
||||
book: String,
|
||||
chapter: i64,
|
||||
verse: i64,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ImportedStrongLink {
|
||||
verse_id: String,
|
||||
position: i64,
|
||||
surface: String,
|
||||
strong: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_library_state(app: AppHandle) -> Result<LibraryState, String> {
|
||||
let conn = open_db(&app)?;
|
||||
let verses = load_verses(&conn, None)?;
|
||||
let tags = load_all_tags(&conn)?;
|
||||
Ok(LibraryState { verses, tags })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn search_verses(app: AppHandle, query: String) -> Result<Vec<Verse>, String> {
|
||||
let conn = open_db(&app)?;
|
||||
let trimmed = query.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return load_verses(&conn, None);
|
||||
}
|
||||
|
||||
if let Some(label) = trimmed.strip_prefix("tag:") {
|
||||
return load_verses_for_tag(&conn, label.trim());
|
||||
}
|
||||
|
||||
if looks_like_strongs(trimmed) {
|
||||
return load_verses_for_strongs(&conn, trimmed);
|
||||
}
|
||||
|
||||
load_verses(&conn, Some(trimmed))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn add_tag_to_verse(
|
||||
app: AppHandle,
|
||||
verse_id: i64,
|
||||
label: String,
|
||||
color: String,
|
||||
) -> Result<Verse, String> {
|
||||
let conn = open_db(&app)?;
|
||||
let normalized = label.trim();
|
||||
if normalized.is_empty() {
|
||||
return Err("Tag label cannot be empty.".to_string());
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO tags (label, color) VALUES (?1, ?2)",
|
||||
params![normalized, color],
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let tag_id: i64 = conn
|
||||
.query_row(
|
||||
"SELECT id FROM tags WHERE lower(label) = lower(?1)",
|
||||
params![normalized],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO verse_tags (verse_id, tag_id) VALUES (?1, ?2)",
|
||||
params![verse_id, tag_id],
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
load_verse(&conn, verse_id)
|
||||
}
|
||||
|
||||
fn open_db(app: &AppHandle) -> Result<Connection, String> {
|
||||
let path = db_path(app)?;
|
||||
let mut conn = Connection::open(path).map_err(|err| err.to_string())?;
|
||||
migrate(&conn)?;
|
||||
seed(&mut conn)?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
fn db_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|err| format!("Unable to resolve app data directory: {err}"))?;
|
||||
fs::create_dir_all(&dir).map_err(|err| err.to_string())?;
|
||||
Ok(dir.join("libre-study.sqlite3"))
|
||||
}
|
||||
|
||||
fn migrate(conn: &Connection) -> Result<(), String> {
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS verses (
|
||||
id INTEGER PRIMARY KEY,
|
||||
book TEXT NOT NULL,
|
||||
chapter INTEGER NOT NULL,
|
||||
verse INTEGER NOT NULL,
|
||||
text TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lexicon (
|
||||
strong TEXT PRIMARY KEY,
|
||||
lemma TEXT NOT NULL,
|
||||
transliteration TEXT NOT NULL,
|
||||
definition TEXT NOT NULL,
|
||||
language TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verse_strongs (
|
||||
verse_id INTEGER NOT NULL,
|
||||
strong TEXT NOT NULL,
|
||||
position INTEGER NOT NULL,
|
||||
PRIMARY KEY (verse_id, strong),
|
||||
FOREIGN KEY (verse_id) REFERENCES verses(id),
|
||||
FOREIGN KEY (strong) REFERENCES lexicon(strong)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY,
|
||||
label TEXT NOT NULL COLLATE NOCASE UNIQUE,
|
||||
color TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verse_tags (
|
||||
verse_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (verse_id, tag_id),
|
||||
FOREIGN KEY (verse_id) REFERENCES verses(id),
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id)
|
||||
);
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn seed(conn: &mut Connection) -> Result<(), String> {
|
||||
let verse_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM verses", [], |row| row.get(0))
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
if verse_count > 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(package_dir) = find_default_package_dir() {
|
||||
import_package(conn, package_dir)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let seed_sql = "
|
||||
INSERT INTO verses (id, book, chapter, verse, text) VALUES
|
||||
(1, 'Genesis', 1, 1, 'In the beginning God created the heaven and the earth.'),
|
||||
(2, 'Psalm', 23, 1, 'The LORD is my shepherd; I shall not want.'),
|
||||
(3, 'John', 3, 16, 'For God so loved the world, that he gave his only begotten Son, that whosoever believeth in him should not perish, but have everlasting life.'),
|
||||
(4, 'Romans', 8, 28, 'And we know that all things work together for good to them that love God, to them who are the called according to his purpose.');
|
||||
|
||||
INSERT INTO lexicon (strong, lemma, transliteration, definition, language) VALUES
|
||||
('H7225', '\u{05e8}\u{05b5}\u{05d0}\u{05e9}\u{05b4}\u{05c1}\u{05d9}\u{05ea}', 'reshith', 'beginning, first, chief, the first part.', 'Hebrew'),
|
||||
('H430', '\u{05d0}\u{05b1}\u{05dc}\u{05b9}\u{05d4}\u{05b4}\u{05d9}\u{05dd}', 'elohim', 'God, gods, judges, rulers; plural form often used of the one true God.', 'Hebrew'),
|
||||
('H1254', '\u{05d1}\u{05b8}\u{05bc}\u{05e8}\u{05b8}\u{05d0}', 'bara', 'to create, shape, form; used of divine creative action.', 'Hebrew'),
|
||||
('H3068', '\u{05d9}\u{05b0}\u{05d4}\u{05d5}\u{05b8}\u{05b9}\u{05d4}', 'YHWH', 'the covenant name of the LORD.', 'Hebrew'),
|
||||
('H7462', '\u{05e8}\u{05b8}\u{05e2}\u{05b8}\u{05d4}', 'raah', 'to shepherd, pasture, feed, tend a flock.', 'Hebrew'),
|
||||
('G25', '\u{1f00}\u{03b3}\u{03b1}\u{03c0}\u{03ac}\u{03c9}', 'agapao', 'to love, value, welcome, or take pleasure in.', 'Greek'),
|
||||
('G2889', '\u{03ba}\u{03cc}\u{03c3}\u{03bc}\u{03bf}\u{03c2}', 'kosmos', 'world, ordered system, inhabitants of the earth.', 'Greek'),
|
||||
('G4100', '\u{03c0}\u{03b9}\u{03c3}\u{03c4}\u{03b5}\u{03cd}\u{03c9}', 'pisteuo', 'to believe, trust, have faith in.', 'Greek'),
|
||||
('G2222', '\u{03b6}\u{03c9}\u{03ae}', 'zoe', 'life; often spiritual and eternal life.', 'Greek'),
|
||||
('G2822', '\u{03ba}\u{03bb}\u{03b7}\u{03c4}\u{03cc}\u{03c2}', 'kletos', 'called, invited, appointed.', 'Greek'),
|
||||
('G4286', '\u{03c0}\u{03c1}\u{03cc}\u{03b8}\u{03b5}\u{03c3}\u{03b9}\u{03c2}', 'prothesis', 'purpose, setting forth, deliberate plan.', 'Greek');
|
||||
|
||||
INSERT INTO verse_strongs (verse_id, strong, position) VALUES
|
||||
(1, 'H7225', 1), (1, 'H430', 2), (1, 'H1254', 3),
|
||||
(2, 'H3068', 1), (2, 'H7462', 2),
|
||||
(3, 'G25', 1), (3, 'G2889', 2), (3, 'G4100', 3), (3, 'G2222', 4),
|
||||
(4, 'G25', 1), (4, 'G2822', 2), (4, 'G4286', 3);
|
||||
";
|
||||
|
||||
conn.execute_batch(seed_sql).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn find_default_package_dir() -> Option<PathBuf> {
|
||||
if let Ok(path) = env::var("LIBRE_BIBLE_DATA_PACKAGE") {
|
||||
let candidate = PathBuf::from(path);
|
||||
if candidate.join("verses.jsonl").exists() && candidate.join("strongs-links.jsonl").exists()
|
||||
{
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
let cwd = env::current_dir().ok()?;
|
||||
let mut candidates = Vec::new();
|
||||
candidates.push(
|
||||
cwd.join("..")
|
||||
.join("libre-bible-data")
|
||||
.join("packages")
|
||||
.join("json")
|
||||
.join("kjv-eng-kjv2006"),
|
||||
);
|
||||
|
||||
if let Some(parent) = cwd.parent() {
|
||||
candidates.push(
|
||||
parent
|
||||
.join("libre-bible-data")
|
||||
.join("packages")
|
||||
.join("json")
|
||||
.join("kjv-eng-kjv2006"),
|
||||
);
|
||||
}
|
||||
|
||||
candidates.into_iter().find(|path| {
|
||||
path.join("verses.jsonl").exists() && path.join("strongs-links.jsonl").exists()
|
||||
})
|
||||
}
|
||||
|
||||
fn import_package(conn: &mut Connection, package_dir: PathBuf) -> Result<(), String> {
|
||||
let verses_path = package_dir.join("verses.jsonl");
|
||||
let strongs_path = package_dir.join("strongs-links.jsonl");
|
||||
let tx = conn.transaction().map_err(|err| err.to_string())?;
|
||||
let mut verse_ids = HashMap::new();
|
||||
|
||||
{
|
||||
let file = File::open(&verses_path)
|
||||
.map_err(|err| format!("Unable to open {}: {err}", verses_path.display()))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for (index, line) in reader.lines().enumerate() {
|
||||
let line = line.map_err(|err| err.to_string())?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let imported: ImportedVerse =
|
||||
serde_json::from_str(&line).map_err(|err| err.to_string())?;
|
||||
let verse_id = (index + 1) as i64;
|
||||
tx.execute(
|
||||
"INSERT INTO verses (id, book, chapter, verse, text) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![
|
||||
verse_id,
|
||||
imported.book,
|
||||
imported.chapter,
|
||||
imported.verse,
|
||||
imported.text
|
||||
],
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
verse_ids.insert(imported.id, verse_id);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let file = File::open(&strongs_path)
|
||||
.map_err(|err| format!("Unable to open {}: {err}", strongs_path.display()))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line.map_err(|err| err.to_string())?;
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let imported: ImportedStrongLink =
|
||||
serde_json::from_str(&line).map_err(|err| err.to_string())?;
|
||||
let Some(verse_id) = verse_ids.get(&imported.verse_id) else {
|
||||
continue;
|
||||
};
|
||||
let language = if imported.strong.starts_with('H') {
|
||||
"Hebrew"
|
||||
} else {
|
||||
"Greek"
|
||||
};
|
||||
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO lexicon (strong, lemma, transliteration, definition, language)
|
||||
VALUES (?1, ?2, '', 'Linked Strong''s number from the source USFM package.', ?3)",
|
||||
params![imported.strong, imported.surface, language],
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
tx.execute(
|
||||
"INSERT OR IGNORE INTO verse_strongs (verse_id, strong, position) VALUES (?1, ?2, ?3)",
|
||||
params![verse_id, imported.strong, imported.position],
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit().map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn load_verses(conn: &Connection, query: Option<&str>) -> Result<Vec<Verse>, String> {
|
||||
let mut verses = Vec::new();
|
||||
|
||||
match query {
|
||||
Some(query) => {
|
||||
let pattern = format!("%{}%", query);
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"
|
||||
SELECT id, book, chapter, verse, text
|
||||
FROM verses
|
||||
WHERE text LIKE ?1
|
||||
OR book || ' ' || chapter || ':' || verse LIKE ?1
|
||||
ORDER BY id
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![pattern], row_to_verse_shell)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
for row in rows {
|
||||
verses.push(hydrate_verse(conn, row.map_err(|err| err.to_string())?)?);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, book, chapter, verse, text FROM verses ORDER BY id")
|
||||
.map_err(|err| err.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_verse_shell)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
for row in rows {
|
||||
verses.push(hydrate_verse(conn, row.map_err(|err| err.to_string())?)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(verses)
|
||||
}
|
||||
|
||||
fn load_verse(conn: &Connection, verse_id: i64) -> Result<Verse, String> {
|
||||
let shell = conn
|
||||
.query_row(
|
||||
"SELECT id, book, chapter, verse, text FROM verses WHERE id = ?1",
|
||||
params![verse_id],
|
||||
row_to_verse_shell,
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
hydrate_verse(conn, shell)
|
||||
}
|
||||
|
||||
fn load_verses_for_tag(conn: &Connection, label: &str) -> Result<Vec<Verse>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"
|
||||
SELECT v.id, v.book, v.chapter, v.verse, v.text
|
||||
FROM verses v
|
||||
JOIN verse_tags vt ON vt.verse_id = v.id
|
||||
JOIN tags t ON t.id = vt.tag_id
|
||||
WHERE lower(t.label) = lower(?1)
|
||||
ORDER BY v.id
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![label], row_to_verse_shell)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let mut verses = Vec::new();
|
||||
for row in rows {
|
||||
verses.push(hydrate_verse(conn, row.map_err(|err| err.to_string())?)?);
|
||||
}
|
||||
Ok(verses)
|
||||
}
|
||||
|
||||
fn load_verses_for_strongs(conn: &Connection, strong: &str) -> Result<Vec<Verse>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"
|
||||
SELECT v.id, v.book, v.chapter, v.verse, v.text
|
||||
FROM verses v
|
||||
JOIN verse_strongs vs ON vs.verse_id = v.id
|
||||
WHERE lower(vs.strong) = lower(?1)
|
||||
ORDER BY v.id
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map(params![strong], row_to_verse_shell)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let mut verses = Vec::new();
|
||||
for row in rows {
|
||||
verses.push(hydrate_verse(conn, row.map_err(|err| err.to_string())?)?);
|
||||
}
|
||||
Ok(verses)
|
||||
}
|
||||
|
||||
fn row_to_verse_shell(row: &rusqlite::Row<'_>) -> rusqlite::Result<Verse> {
|
||||
Ok(Verse {
|
||||
id: row.get(0)?,
|
||||
book: row.get(1)?,
|
||||
chapter: row.get(2)?,
|
||||
verse: row.get(3)?,
|
||||
text: row.get(4)?,
|
||||
strongs: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn hydrate_verse(conn: &Connection, mut verse: Verse) -> Result<Verse, String> {
|
||||
verse.strongs = load_strongs(conn, verse.id)?;
|
||||
verse.tags = load_tags(conn, verse.id)?;
|
||||
Ok(verse)
|
||||
}
|
||||
|
||||
fn load_strongs(conn: &Connection, verse_id: i64) -> Result<Vec<StrongLink>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"
|
||||
SELECT l.strong, l.lemma, l.transliteration, l.definition, l.language
|
||||
FROM verse_strongs vs
|
||||
JOIN lexicon l ON l.strong = vs.strong
|
||||
WHERE vs.verse_id = ?1
|
||||
ORDER BY vs.position
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![verse_id], |row| {
|
||||
Ok(StrongLink {
|
||||
strong: row.get(0)?,
|
||||
lemma: row.get(1)?,
|
||||
transliteration: row.get(2)?,
|
||||
definition: row.get(3)?,
|
||||
language: row.get(4)?,
|
||||
})
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for row in rows {
|
||||
entries.push(row.map_err(|err| err.to_string())?);
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn load_tags(conn: &Connection, verse_id: i64) -> Result<Vec<Tag>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"
|
||||
SELECT t.id, t.label, t.color
|
||||
FROM verse_tags vt
|
||||
JOIN tags t ON t.id = vt.tag_id
|
||||
WHERE vt.verse_id = ?1
|
||||
ORDER BY t.label
|
||||
",
|
||||
)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let rows = stmt
|
||||
.query_map(params![verse_id], |row| {
|
||||
Ok(Tag {
|
||||
id: row.get(0)?,
|
||||
label: row.get(1)?,
|
||||
color: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let mut tags = Vec::new();
|
||||
for row in rows {
|
||||
tags.push(row.map_err(|err| err.to_string())?);
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn load_all_tags(conn: &Connection) -> Result<Vec<Tag>, String> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, label, color FROM tags ORDER BY label")
|
||||
.map_err(|err| err.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(Tag {
|
||||
id: row.get(0)?,
|
||||
label: row.get(1)?,
|
||||
color: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let mut tags = Vec::new();
|
||||
for row in rows {
|
||||
tags.push(row.map_err(|err| err.to_string())?);
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn looks_like_strongs(query: &str) -> bool {
|
||||
let mut chars = query.chars();
|
||||
matches!(chars.next(), Some('G' | 'g' | 'H' | 'h')) && chars.all(|char| char.is_ascii_digit())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
}
|
||||
Reference in New Issue
Block a user