24 Commits

Author SHA1 Message Date
zxq5 bc7cf6b8ed fully abstracted away filesystem with LegacyFileSystem trait. next I need to start updating to an ID based save/load system with a ModernFileSystem trait
Continuous integration / build (push) Successful in 1m35s
2025-08-22 00:44:54 +01:00
zxq5 7272d19207 added filesystem abstraction layer and implemented basic filesystem - testing not complete yet, still features missing
Continuous integration / build (push) Successful in 3m27s
2025-08-21 23:11:09 +01:00
zxq5 9614d2884b added filesystem abstraction layer and implemented basic filesystem - testing not complete yet, still features missing
Continuous integration / build (push) Has been cancelled
2025-08-21 23:07:46 +01:00
zxq5 c891a8be58 ui improvements and feature flags for AI integration
Continuous integration / build (push) Failing after 4m9s
2025-08-18 01:06:30 +01:00
zxq5 b6d0e10a7d UI fix
Continuous integration / build (push) Has been cancelled
2025-08-11 01:34:27 +01:00
zxq5 8cf54f3346 moved ai assistant to editor side panel
Continuous integration / build (push) Successful in 1m32s
2025-08-10 00:55:02 +01:00
zxq5 874d7ff377 updated .gitignore
Continuous integration / build (push) Successful in 1m32s
2025-08-09 23:44:21 +01:00
zxq5 3c47ae1305 using a gitea compatible upload-artifact action
Continuous integration / build (push) Successful in 1m41s
2025-08-09 23:20:47 +01:00
zxq5 67d8902eaf using a gitea compatible upload-artifact action
Continuous integration / build (push) Failing after 13s
2025-08-09 23:19:49 +01:00
zxq5 2a7ec348c5 updated repo name in actiosn
Continuous integration / build (push) Failing after 6m19s
2025-08-09 23:03:15 +01:00
zxq5 0653427557 updated repo name in actiosn
Continuous integration / build (push) Successful in 6m17s
2025-08-09 22:56:26 +01:00
zxq5 6b6f65713d commented out windows target
Continuous integration / build (push) Successful in 1m32s
2025-08-09 22:47:43 +01:00
zxq5 c21819e786 fixed cargo warns
Continuous integration / build (push) Failing after 1m48s
2025-08-09 22:42:42 +01:00
zxq5 cc7eb3e7fb updated gitignore and setup PKGBUILD for archlinux packaging
Continuous integration / build (push) Failing after 7m6s
2025-08-09 22:35:05 +01:00
zxq5 745e03a74f idk what i changed
Continuous integration / build (push) Has been cancelled
2025-07-30 22:47:02 +01:00
zxq5 e5a485d3a7 reworked settings, general interface improvements, more AI configuration, bugfixes and QOL.
Continuous integration / build (push) Has been cancelled
2025-07-30 02:48:49 +01:00
zxq5 6c40f34122 added streaming for AI content generation
Continuous integration / build (push) Failing after 6m27s
2025-07-28 01:27:03 +01:00
zxq5 5294feb5ff updated roadmap
Continuous integration / build (push) Failing after 6m28s
2025-07-28 00:05:29 +01:00
zxq5 6832f1c5bc test
Continuous integration / build (push) Has been cancelled
2025-07-28 00:03:24 +01:00
zxq5 65213d3a9c test
Continuous integration / build (push) Has been cancelled
2025-07-28 00:03:04 +01:00
zxq5 73d5654e25 removed unnecessary
Continuous integration / build (push) Has been cancelled
2025-07-28 00:02:00 +01:00
zxq5 71f8f76f99 added font to repo
Continuous integration / build (push) Has been cancelled
2025-07-28 00:01:16 +01:00
zxq5 4d0e0c90a7 minor changes
Continuous integration / build (push) Failing after 6m40s
2025-07-27 23:23:47 +01:00
zxq5 bc71a30bfa fixed ai integration
Continuous integration / build (push) Failing after 6m27s
2025-07-27 19:15:30 +01:00
40 changed files with 1716 additions and 1651 deletions
+9 -9
View File
@@ -9,17 +9,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with: # with:
target: x86_64-pc-windows-gnu # target: x86_64-pc-windows-gnu
- run: cargo build --release - run: cargo build --release
- run: cargo build --release --target x86_64-pc-windows-gnu # - run: cargo build --release --target x86_64-pc-windows-gnu
- uses: actions/upload-artifact@v4 - uses: christopherhx/gitea-upload-artifact@v4
with: with:
name: linux-release name: linux-release
path: target/release/doc_writing_tool path: target/release/worldcoder
- uses: actions/upload-artifact@v4 # - uses: actions/upload-artifact@v4
with: # with:
name: windows-release # name: windows-release
path: target/x86_64-pc-windows-gnu/release/doc_writing_tool.exe # path: target/x86_64-pc-windows-gnu/release/doc_writing_tool.exe
+4
View File
@@ -1,3 +1,7 @@
/target /target
*/target */target
/project /project
Cargo.lock
*.pkg.tar.zst
/pkg
/.config
+7 -2
View File
@@ -1,9 +1,14 @@
{ {
"rust-analyzer.check.command": "clippy", "rust-analyzer.check.command": "clippy",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"rust-analyzer.cargo.features": "all",
"files.eol": "\n", "files.eol": "\n",
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.trimFinalNewlines": true, "files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true,
"rust-analyzer.cargo.features": [
"native",
"llm"
],
"rust-analyzer.cargo.noDefaultFeatures": true,
"rust-analyzer.cargo.allFeatures": false
} }
Generated
+188 -199
View File
File diff suppressed because it is too large Load Diff
+11 -10
View File
@@ -1,12 +1,11 @@
[package] [package]
name = "doc_writing_tool" name = "worldcoder"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
eframe = "0.32.0" eframe = "0.32.0"
egui = { version = "0.32.0", features = ["serde"] } egui = { version = "0.32.0", features = ["serde"] }
editor = { path = "./editor" }
egui_extras = { version = "0.32.0", features = [ egui_extras = { version = "0.32.0", features = [
"chrono", "chrono",
"datepicker", "datepicker",
@@ -16,15 +15,17 @@ egui_extras = { version = "0.32.0", features = [
egui_file = "0.23.0" egui_file = "0.23.0"
image = { version = "0.25.6", features = ["jpeg", "png"] } image = { version = "0.25.6", features = ["jpeg", "png"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140" serde_json = "1.0.141"
chrono = { version = "0.4.41", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
thiserror = "2.0.12" thiserror = "2.0.14"
egui_commonmark = { version = "0.21.1", features = ["embedded_image"] } egui_commonmark = { version = "0.21.1", features = ["embedded_image"] }
walkdir = "2.5.0"
uuid = { version = "1.17.0", features = ["v4"] } uuid = { version = "1.17.0", features = ["v4"] }
reqwest = { version = "0.12.22", features = ["blocking", "json"] } reqwest = { version = "0.12.23", features = ["blocking", "json"] }
tempfile = "3.20.0"
itertools = "0.14.0"
[features]
[target.x86_64-pc-windows-gnu] default = ["native", "llm"]
linker = "x86_64-w64-mingw32-gcc" web = []
ar = "x86_64-w64-mingw32-gcc-ar" native = []
llm = []
+24
View File
@@ -0,0 +1,24 @@
pkgname=worldcoder
pkgver=0.1.1
pkgrel=3
makedepends=('rust' 'cargo')
arch=('i686' 'x86_64' 'armv6h' 'armv7h')
prepare() {
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
}
build() {
export RUSTUP_TOOLCHAIN=stable
export CARGO_TARGET_DIR=target
cargo build --frozen --release --all-features
}
check() {
export RUSTUP_TOOLCHAIN=stable
cargo test --frozen --all-features
}
package() {
install -Dm0755 -t "$pkgdir/usr/bin/" "target/release/$pkgname"
}
-354
View File
@@ -1,354 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ab_glyph"
version = "0.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0f4f6fbdc5ee39f2ede9f5f3ec79477271a6d6a2baff22310d51736bda6cea"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser",
]
[[package]]
name = "ab_glyph_rasterizer"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "ecolor"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a631732d995184114016fab22fc7e3faf73d6841c2d7650395fe251fbcd9285"
dependencies = [
"emath",
]
[[package]]
name = "editor"
version = "0.1.0"
dependencies = [
"egui",
"serde",
]
[[package]]
name = "egui"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8470210c95a42cc985d9ffebfd5067eea55bdb1c3f7611484907db9639675e28"
dependencies = [
"ahash",
"bitflags",
"emath",
"epaint",
"nohash-hasher",
"profiling",
"smallvec",
"unicode-segmentation",
]
[[package]]
name = "emath"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45f057b141e7e46340c321400be74b793543b1b213036f0f989c35d35957c32e"
[[package]]
name = "epaint"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94cca02195f0552c17cabdc02f39aa9ab6fbd815dac60ab1cd3d5b0aa6f9551c"
dependencies = [
"ab_glyph",
"ahash",
"ecolor",
"emath",
"epaint_default_fonts",
"nohash-hasher",
"parking_lot",
"profiling",
]
[[package]]
name = "epaint_default_fonts"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8495e11ed527dff39663b8c36b6c2b2799d7e4287fb90556e455d72eca0b4d3"
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "owned_ttf_parser"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
dependencies = [
"ttf-parser",
]
[[package]]
name = "parking_lot"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
"bitflags",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
-13
View File
@@ -1,13 +0,0 @@
[package]
name = "editor"
version = "0.1.0"
edition = "2024"
description = "a basic text editor widget with line numbers"
[dependencies]
egui = "0.32.0"
serde = "1"
[lib]
name = "editor"
path = "src/lib.rs"
-214
View File
@@ -1,214 +0,0 @@
use egui::Color32;
use egui::TextBuffer;
use egui::widgets::text_edit::TextEditOutput;
use std::hash::{Hash, Hasher};
#[derive(Clone, Debug, PartialEq)]
/// CodeEditor struct which stores settings for highlighting.
pub struct CodeEditor {
id: String,
numlines: bool,
numlines_shift: isize,
numlines_only_natural: bool,
fontsize: f32,
stick_to_bottom: bool,
desired_width: f32,
}
impl Hash for CodeEditor {
fn hash<H: Hasher>(&self, state: &mut H) {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
(self.fontsize as u32).hash(state);
}
}
impl Default for CodeEditor {
fn default() -> CodeEditor {
CodeEditor {
id: String::from("Code Editor"),
numlines: true,
numlines_shift: 0,
numlines_only_natural: false,
fontsize: 10.0,
stick_to_bottom: false,
desired_width: f32::INFINITY,
}
}
}
impl CodeEditor {
pub fn id_source(self, id_source: impl Into<String>) -> Self {
CodeEditor {
id: id_source.into(),
..self
}
}
/// Use custom font size
///
/// **Default: 10.0**
pub fn with_fontsize(self, fontsize: f32) -> Self {
CodeEditor { fontsize, ..self }
}
/// Use UI font size
pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
CodeEditor {
fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
..self
}
}
/// Show or hide lines numbering
///
/// **Default: true**
pub fn with_numlines(self, numlines: bool) -> Self {
CodeEditor { numlines, ..self }
}
/// Shift lines numbering by this value
///
/// **Default: 0**
pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
CodeEditor {
numlines_shift,
..self
}
}
/// Show lines numbering only above zero, useful for enabling numbering since nth row
///
/// **Default: false**
pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
CodeEditor {
numlines_only_natural,
..self
}
}
/// Should the containing area shrink if the content is small?
///
/// **Default: false**
pub fn auto_shrink(self, shrink: bool) -> Self {
CodeEditor {
desired_width: if shrink { 0.0 } else { self.desired_width },
..self
}
}
/// Sets the desired width of the code editor
///
/// **Default: `f32::INFINITY`**
pub fn desired_width(self, width: f32) -> Self {
CodeEditor {
desired_width: width,
..self
}
}
/// Stick to bottom
/// The scroll handle will stick to the bottom position even while the content size
/// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers.
/// The scroll handle remains stuck until user manually changes position. Once "unstuck"
/// it will remain focused on whatever content viewport the user left it on. If the scroll
/// handle is dragged to the bottom it will again become stuck and remain there until manually
/// pulled from the end position.
///
/// **Default: false**
pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
CodeEditor {
stick_to_bottom,
..self
}
}
pub fn format(&self, _string: &str) -> egui::text::TextFormat {
let font_id = egui::FontId::monospace(self.fontsize);
let color = Color32::WHITE;
egui::text::TextFormat::simple(font_id, color)
}
fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
let total = if text.ends_with('\n') || text.is_empty() {
text.lines().count() + 1
} else {
text.lines().count()
} as isize;
let max_indent = total
.to_string()
.len()
.max(!self.numlines_only_natural as usize * self.numlines_shift.to_string().len());
let mut counter = (1..=total)
.map(|i| {
let num = i + self.numlines_shift;
if num <= 0 && self.numlines_only_natural {
String::new()
} else {
let label = num.to_string();
format!(
"{}{label}",
" ".repeat(max_indent.saturating_sub(label.len()))
)
}
})
.collect::<Vec<String>>()
.join("\n");
#[allow(clippy::cast_precision_loss)]
let width = max_indent as f32
* self.fontsize
* 0.5
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8 as f32;
let mut layouter = |ui: &egui::Ui, string: &dyn TextBuffer, _wrap_width: f32| {
let layout_job = egui::text::LayoutJob::single_section(
string.as_str().to_string(), // Convert TextBuffer to String
egui::TextFormat::simple(egui::FontId::monospace(self.fontsize), Color32::WHITE),
);
ui.fonts(|f| f.layout_job(layout_job))
};
ui.add(
egui::TextEdit::multiline(&mut counter)
.id_source(format!("{}_numlines", self.id))
.font(egui::TextStyle::Monospace)
.interactive(false)
.frame(false)
.desired_width(width)
.layouter(&mut layouter),
);
}
/// Show Code Editor
pub fn show(&mut self, ui: &mut egui::Ui, text: &mut dyn egui::TextBuffer) -> TextEditOutput {
let mut text_edit_output: Option<TextEditOutput> = None;
let code_editor = |ui: &mut egui::Ui| {
ui.horizontal_top(|h| {
if self.numlines {
self.numlines_show(h, text.as_str());
}
egui::ScrollArea::horizontal()
.hscroll(true)
.id_salt(format!("{}_inner_scroll", self.id))
.show(h, |ui| {
let output = egui::TextEdit::multiline(text)
.id_source(&self.id)
.lock_focus(true)
.frame(false)
.desired_width(self.desired_width)
.show(ui);
text_edit_output = Some(output);
});
});
};
egui::ScrollArea::vertical()
.id_salt(format!("{}_outer_scroll", self.id))
.stick_to_bottom(self.stick_to_bottom)
.show(ui, code_editor);
text_edit_output.expect("TextEditOutput should exist at this point")
}
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

-6
View File
@@ -1,6 +0,0 @@
{
"date": "2025-07-17",
"project_name": "New Project",
"project_author": "Your Name",
"project_description": "Description of your project"
}
@@ -1,8 +0,0 @@
{
"title": "test",
"id": "83592caa-f97d-427e-9d6a-50a586c30e6e",
"description": "ee",
"tags": [],
"content": "# Test project\n\n- this project is a test to ensure that this tool can be integrated with AI models correctly\n- Im testing various prompts and parameters to evaluate its capabilities. The initial focus is on simple tasks like list generation, text summarization, and question answering. More complex scenarios involving code generation and creative writing will follow in subsequent phases. A key aspect of this test project involves documenting all interactions both the prompts used and the AIs responses for later analysis. This allows us to identify patterns, biases, and areas where the tool can be improved. ",
"parent": null
}
@@ -1,23 +0,0 @@
{
"id": "161227ef-ba29-41a7-b40a-ed4ac550a8ea",
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
"name": "nucleus",
"fields": {
"age": {
"Number": 0.0
},
"parent": {
"Link": ""
},
"dob": {
"Date": "1970-01-01"
},
"pfp": {
"Image": "characters/nucleus.png"
},
"description": {
"MultiLine": "an AI"
}
},
"tags": []
}
@@ -1,23 +0,0 @@
{
"id": "3ce0e977-9f65-4f4c-a036-67f3d5c25fdc",
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
"name": "ZXQ5",
"fields": {
"dob": {
"Date": "1970-01-01"
},
"description": {
"MultiLine": "yes"
},
"age": {
"Number": 19.1
},
"parent": {
"Link": ""
},
"pfp": {
"Image": "characters/zxq5.png"
}
},
"tags": []
}
@@ -1,23 +0,0 @@
{
"id": "57429207-5fc1-4bab-a524-c550773c3d45",
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
"name": "Tayles",
"fields": {
"pfp": {
"Image": "characters/tayles.png"
},
"parent": {
"Link": ""
},
"description": {
"MultiLine": "trainspotter"
},
"age": {
"Number": 17.5
},
"dob": {
"Date": "1970-01-01"
}
},
"tags": []
}
@@ -1,25 +0,0 @@
{
"id": "be24e58f-3f79-4c5a-9224-9037eea5f51f",
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
"name": "The Order",
"fields": {
"pfp": {
"Image": "characters/the order.png"
},
"description": {
"MultiLine": "yes"
},
"dob": {
"Date": "1970-01-29"
},
"parent": {
"Link": ""
},
"age": {
"Number": 20.6
}
},
"tags": [
"bbeddabd-914c-4648-8262-bf14bfcf8fff"
]
}
@@ -1,25 +0,0 @@
{
"id": "deeaee92-bdec-4eb3-bb3b-ee760fc83d45",
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
"name": "The Chancellor",
"fields": {
"age": {
"Number": 37.0
},
"parent": {
"Link": ""
},
"dob": {
"Date": "1970-01-01"
},
"description": {
"MultiLine": "a tall ahh american"
},
"pfp": {
"Image": "characters/the chancellor.jpg"
}
},
"tags": [
"bbeddabd-914c-4648-8262-bf14bfcf8fff"
]
}
@@ -1,11 +0,0 @@
{
"id": "bbeddabd-914c-4648-8262-bf14bfcf8fff",
"name": "American",
"description": "an american smh",
"color": [
0,
32,
207,
255
]
}
@@ -1,32 +0,0 @@
{
"name": "Species",
"id": "353649f9-e1f3-46d9-b723-8e56b510b2cc",
"description": "A classification system for living or digital entities.",
"fields": [
{
"name": "Diverged from",
"field_type": {
"Link": {
"template_id": null
}
},
"required": false,
"on_preview": false,
"description": "did this diverge from another documented species?"
},
{
"name": "Appearance / Features",
"field_type": "MultiLine",
"required": true,
"on_preview": true,
"description": "anatomy etc."
},
{
"name": "behaviour",
"field_type": "MultiLine",
"required": true,
"on_preview": true,
"description": "aggressive, collaborative, etc.."
}
]
}
@@ -1,46 +0,0 @@
{
"name": "Character",
"id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
"description": "a character",
"fields": [
{
"name": "description",
"field_type": "MultiLine",
"required": true,
"on_preview": true,
"description": "yes"
},
{
"name": "age",
"field_type": "Number",
"required": true,
"on_preview": false,
"description": "yes"
},
{
"name": "dob",
"field_type": "Date",
"required": true,
"on_preview": false,
"description": "yes"
},
{
"name": "parent",
"field_type": {
"Link": {
"template_id": null
}
},
"required": true,
"on_preview": false,
"description": "yes"
},
{
"name": "pfp",
"field_type": "Image",
"required": true,
"on_preview": true,
"description": "yes"
}
]
}
+6 -6
View File
@@ -24,9 +24,9 @@
### v0.1.1 - Editor ### v0.1.1 - Editor
- [ ] Basic editor (markdown formatting) - [x] Basic editor (markdown formatting)
- [x] Basic text editing - [x] Basic text editing
- [ ] Load & Save text file - [x] Load & Save text file
- [x] Editor preview - [x] Editor preview
- [x] Preview text in markdown - [x] Preview text in markdown
@@ -34,13 +34,13 @@
### v0.2.0 - links & context building ### v0.2.0 - links & context building
- [ ] Links between objects - [x] Links between objects
- [ ] Links in templates - [ ] Links in templates
### v0.2.1 - writing projects & organisation ### v0.2.1 - writing projects & organisation
- [ ] Project management - [x] Project management
- [ ] Chapters/organisation - [x] Chapters/organisation
## v0.3 - Workflows & AI integration ## v0.3 - Workflows & AI integration
@@ -70,7 +70,7 @@
- [ ] Content generation AI - [ ] Content generation AI
- [ ] Collect context from across a story or project - [ ] Collect context from across a story or project
- [ ] Generate content for stories - [x] Generate content for stories (Done to a very basic level)
- [ ] Create original content in the form of objects using templates - [ ] Create original content in the form of objects using templates
- [ ] Create a new template - [ ] Create a new template
+9 -6
View File
@@ -1,6 +1,10 @@
use egui::{TextEdit, vec2}; use egui::{TextEdit, vec2};
use crate::{PROJECT_FOLDER, util}; use crate::{
PROJECT_FOLDER,
filesystem::{FILESYSTEM, FsError, LegacyFileSystem},
util,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Asset { pub struct Asset {
@@ -25,15 +29,14 @@ impl Asset {
println!("old_path: {old_path:?}"); println!("old_path: {old_path:?}");
println!("new_path: {new_path:?}"); println!("new_path: {new_path:?}");
// move from src dir to name path if let Err(FsError::Io(err)) = FILESYSTEM.rename(&old_path, &new_path) {
if let Err(err) = std::fs::rename(&old_path, &new_path) {
match err.kind() { match err.kind() {
std::io::ErrorKind::NotFound => { std::io::ErrorKind::NotFound => {
let dir = new_path.parent().unwrap(); let dir = new_path.parent().unwrap();
if !dir.exists() { if !dir.exists() {
std::fs::create_dir_all(dir).unwrap(); FILESYSTEM.mkdir(dir).unwrap();
} }
std::fs::rename(&old_path, &new_path).unwrap(); FILESYSTEM.rename(&old_path, &new_path).unwrap();
} }
_ => panic!("Failed to rename file: {err}"), _ => panic!("Failed to rename file: {err}"),
} }
@@ -73,7 +76,7 @@ impl Asset {
ui.separator(); ui.separator();
if let Ok(bytes) = std::fs::read(Self::path(&self.name)) { if let Ok(bytes) = FILESYSTEM.read_bytes(&Self::path(&self.name)) {
let image_source = egui::ImageSource::Bytes { let image_source = egui::ImageSource::Bytes {
uri: std::borrow::Cow::Owned(self.name.clone()), uri: std::borrow::Cow::Owned(self.name.clone()),
bytes: bytes.into(), bytes: bytes.into(),
+195 -168
View File
@@ -1,34 +1,51 @@
use std::path::Path;
use egui::TextEdit; use egui::TextEdit;
use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
use serde::{self, Deserialize, Serialize}; use serde::{self, Deserialize, Serialize};
use crate::{PROJECT_FOLDER, editors::tags::Tag, llm_integration::content_llm::ai_enabled, util}; use crate::{
editors::{settings_editor::ProjectSettings, tags::Tag},
filesystem::{FILESYSTEM, LegacyFileSystem},
util,
};
#[cfg(feature = "llm")]
use crate::llm_integration::content_llm::{ContentAI, ReadyState};
pub struct MainEditor { pub struct MainEditor {
pub content: ContentSection, pub content: ContentSection,
pub show_editor: bool, pub show_editor: bool,
pub editor_separate_window: bool,
pub show_preview: bool, pub show_preview: bool,
preview_cache: CommonMarkCache, preview_cache: CommonMarkCache,
#[cfg(feature = "llm")]
dialog: ContentAI,
#[cfg(feature = "llm")]
pub show_ai: bool,
} }
impl Clone for MainEditor { impl Clone for MainEditor {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
content: self.content.clone(), content: self.content.clone(),
show_editor: self.show_editor, show_editor: self.show_editor,
editor_separate_window: self.editor_separate_window,
show_preview: self.show_preview, show_preview: self.show_preview,
preview_cache: CommonMarkCache::default(), preview_cache: CommonMarkCache::default(),
#[cfg(feature = "llm")]
dialog: self.dialog.clone(),
#[cfg(feature = "llm")]
show_ai: self.show_ai,
} }
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ContentSection { pub struct ContentSection {
#[serde(default)]
pub title: String, pub title: String,
#[serde(default)]
pub id: String, pub id: String,
#[serde(default)] #[serde(default)]
@@ -62,21 +79,16 @@ impl ContentSection {
} }
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> { pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER FILESYSTEM.write(
.join("documents") Path::new(&format!("documents/{id}.json", id = &self.id)),
.join(format!("{}.json", &self.id)); self.clone(),
)?;
let content = serde_json::to_string_pretty(self)?;
std::fs::write(path, content)?;
self.saved = true; self.saved = true;
Ok(()) Ok(())
} }
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER.join("documents").join(format!("{id}.json")); let mut section: Self = FILESYSTEM.read(Path::new(&format!("documents/{id}.json")))?;
let content = std::fs::read_to_string(&path)?;
let mut section: Self = serde_json::from_str(&content)?;
section.saved = true; section.saved = true;
section.id = id.to_string(); section.id = id.to_string();
Ok(section) Ok(section)
@@ -96,7 +108,13 @@ impl MainEditor {
content: ContentSection::new(), content: ContentSection::new(),
show_editor: false, // Start with editor hidden show_editor: false, // Start with editor hidden
show_preview: false, show_preview: false,
editor_separate_window: false,
preview_cache: CommonMarkCache::default(), preview_cache: CommonMarkCache::default(),
#[cfg(feature = "llm")]
show_ai: false,
#[cfg(feature = "llm")]
dialog: ContentAI::new(String::new()),
} }
} }
@@ -105,125 +123,164 @@ impl MainEditor {
content, content,
show_editor: true, show_editor: true,
show_preview: false, show_preview: false,
editor_separate_window: false,
preview_cache: CommonMarkCache::default(), preview_cache: CommonMarkCache::default(),
#[cfg(feature = "llm")]
show_ai: false,
#[cfg(feature = "llm")]
dialog: ContentAI::new(String::new()),
} }
} }
pub fn ui(&mut self, ctx: &egui::Context) { pub fn render_ui(&mut self, project: &mut ProjectSettings, ui: &mut egui::Ui) {
ui.vertical(|ui| {
// check for Ctrl+S to save
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
if let Err(e) = self.content.save() {
eprintln!("Failed to save: {e}");
}
}
// display save state
util::saved_status(
ui,
self.content.saved,
&self.content.id,
&self.content.title,
);
// Save/Cancel buttons
ui.horizontal(|ui| {
// save button
if ui.button("Save").clicked() {
if let Err(e) = self.content.save() {
eprintln!("Failed to save: {e}");
}
}
// create copy button
if ui.button("Create Copy").clicked() {
let mut copy = self.clone();
copy.content.id = uuid::Uuid::new_v4().to_string();
copy.content.title = format!("{} (Copy)", self.content.title);
copy.content.save().unwrap();
}
// delete button
if ui.button("Delete").clicked() {
FILESYSTEM
.delete(Path::new(&format!(
"documents/{id}.json",
id = &self.content.id
)))
.unwrap();
*self = Self::new();
}
// revert changes button
if ui.button("Revert changes").clicked() {
self.content = ContentSection::load(&self.content.id).unwrap();
}
// preview toggle
ui.checkbox(&mut self.show_preview, "Preview");
// assistant toggle
#[cfg(feature = "llm")]
if project.ai_enabled() {
ui.checkbox(&mut self.show_ai, "AI Assistant");
}
// editor toggle
ui.checkbox(&mut self.editor_separate_window, "Pop out editor");
});
});
ui.separator();
// Name and description grid
egui::Grid::new("top_grid")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
ui.strong("Name");
if ui
.add(
TextEdit::singleline(&mut self.content.title)
.desired_width(f32::INFINITY)
.frame(false),
)
.changed()
{
self.content.saved = false;
}
ui.end_row();
ui.strong("Description");
if ui
.add(
TextEdit::singleline(&mut self.content.description)
.desired_width(f32::INFINITY)
.frame(false),
)
.changed()
{
self.content.saved = false;
}
ui.end_row();
ui.strong("Tags");
Tag::selector_ui(&mut self.content.tags, ui, Some(&mut self.content.saved));
ui.end_row();
});
ui.separator();
#[cfg(feature = "llm")]
if self.show_ai && project.ai_enabled() {
let dialog = &mut self.dialog;
dialog.content = self.content.content.clone();
dialog.ui(ui, project);
if *dialog.ready.lock().unwrap() == ReadyState::Ready {
self.content
.content
.push_str(&dialog.result.lock().unwrap());
self.content.saved = false;
*dialog.ready.lock().unwrap() = ReadyState::Idle;
} else if *dialog.ready.lock().unwrap() == ReadyState::Halted {
*dialog.ready.lock().unwrap() = ReadyState::Idle;
}
}
if self.show_preview {
self.preview_ui(ui);
}
self.editor_ui(ui, project);
}
pub fn ui(&mut self, ctx: &egui::Context, project: &mut ProjectSettings) {
// Show the editor window if enabled // Show the editor window if enabled
let mut show = self.show_editor; let mut show = self.show_editor;
if show { if show {
egui::Window::new("Markdown Editor") if self.editor_separate_window {
.resizable(true) egui::Window::new("Editor")
.default_width(1000.0) .resizable(true)
.default_height(800.0) .default_width(1000.0)
.open(&mut show) .default_height(800.0)
.show(ctx, |ui| { .open(&mut show)
ui.vertical(|ui| { .show(ctx, |ui| {
// check for Ctrl+S to save self.render_ui(project, ui);
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
if let Err(e) = self.content.save() {
eprintln!("Failed to save: {e}");
}
}
// display save state
util::saved_status(
ui,
self.content.saved,
&self.content.id,
&self.content.title,
);
// Save/Cancel buttons
ui.horizontal(|ui| {
// save button
if ui.button("Save").clicked() {
if let Err(e) = self.content.save() {
eprintln!("Failed to save: {e}");
}
}
// create copy button
if ui.button("Create Copy").clicked() {
let mut copy = self.clone();
copy.content.id = uuid::Uuid::new_v4().to_string();
copy.content.title = format!("{} (Copy)", self.content.title);
copy.content.save().unwrap();
}
// delete button
if ui.button("Delete").clicked() {
std::fs::remove_file(
PROJECT_FOLDER
.join("documents")
.join(format!("{}.json", self.content.id)),
)
.unwrap();
*self = Self::new();
}
// revert changes button
if ui.button("Revert changes").clicked() {
self.content = ContentSection::load(&self.content.id).unwrap();
}
// preview toggle
ui.checkbox(&mut self.show_preview, "Preview");
});
}); });
} else {
ui.separator(); egui::CentralPanel::default().show(ctx, |ui| {
self.render_ui(project, ui);
// Name and description grid
egui::Grid::new("top_grid")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
ui.strong("Name");
if ui
.add(
TextEdit::singleline(&mut self.content.title)
.desired_width(f32::INFINITY)
.frame(false),
)
.changed()
{
self.content.saved = false;
}
ui.end_row();
ui.strong("Description");
if ui
.add(
TextEdit::singleline(&mut self.content.description)
.desired_width(f32::INFINITY)
.frame(false),
)
.changed()
{
self.content.saved = false;
}
ui.end_row();
ui.strong("Tags");
Tag::selector_ui(
&mut self.content.tags,
ui,
Some(&mut self.content.saved),
);
ui.end_row();
});
ui.separator();
if self.show_preview {
self.preview_ui(ui);
}
self.editor_ui(ui);
}); });
}
} }
self.show_editor = show; self.show_editor = show;
@@ -263,7 +320,7 @@ impl MainEditor {
}); });
} }
fn editor_ui(&mut self, ui: &mut egui::Ui) { fn editor_ui(&mut self, ui: &mut egui::Ui, _project: &mut ProjectSettings) {
let _response = egui::ScrollArea::both() let _response = egui::ScrollArea::both()
.auto_shrink([false, false]) .auto_shrink([false, false])
.id_salt("editor_scroll") .id_salt("editor_scroll")
@@ -281,48 +338,18 @@ impl MainEditor {
ui.set_min_width(max_width as f32); ui.set_min_width(max_width as f32);
let text_edit = TextEdit::multiline(&mut self.content.content) let response = ui.add(
.id_source("MainEditor_editor") TextEdit::multiline(&mut self.content.content)
.font(egui::TextStyle::Monospace) .id_source("MainEditor_editor")
.interactive(true) .font(egui::TextStyle::Monospace)
.frame(false) .interactive(true)
.lock_focus(true) .frame(false)
.hint_text("Type here...") .lock_focus(true)
.desired_width(max_width as f32); .hint_text("Type here...")
.desired_width(max_width as f32),
let mut ctx_menu = false; );
let response = ui if response.changed() {
.add_sized( self.content.saved = false;
egui::vec2(max_width as f32 - 30.0, ui.available_height()),
text_edit,
)
.on_hover_text("Right click to open context menu")
.context_menu(|ui| {
ctx_menu = true;
ui.menu_button("AI Actions", |ui| {
ui.add_enabled_ui(ai_enabled(), |ui| {
if ui.button("Summarise").clicked() {
println!("Summarise");
}
if ui.button("Continue").clicked() {
let content = self.content.content.clone();
let response =
crate::llm_integration::content_llm::continue_content(
&content, "", 1024,
)
.unwrap();
self.content.content.push_str(&response);
}
});
});
});
if let Some(response) = response {
if response.response.changed() || ctx_menu {
self.content.saved = false;
}
} }
}); });
}); });
-80
View File
@@ -1,80 +0,0 @@
use std::io::Read;
use chrono::NaiveDate;
use egui_extras::DatePickerButton;
use serde::{Deserialize, Serialize};
use crate::PROJECT_FOLDER;
#[derive(Serialize, Deserialize)]
pub struct ProjectContext {
date: NaiveDate,
project_name: String,
project_author: String,
project_description: String,
}
impl ProjectContext {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Self {
let path = PROJECT_FOLDER.join("context.json");
if let Ok(mut file) = std::fs::File::open(path) {
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
if let Ok(proj) = serde_json::from_str(&contents) {
return proj;
}
}
Self::default()
}
pub fn save(&self) {
let path = PROJECT_FOLDER.join("context.json");
let content = serde_json::to_string_pretty(self).unwrap();
std::fs::write(path, content).unwrap();
}
#[allow(dead_code)]
pub fn ui(&mut self, ui: &mut egui::Ui) {
// table
egui::Grid::new("context_editor")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
ui.label("Project Name");
ui.text_edit_singleline(&mut self.project_name);
ui.end_row();
ui.label("Project Author");
ui.text_edit_singleline(&mut self.project_author);
ui.end_row();
ui.label("Project Description");
ui.text_edit_singleline(&mut self.project_description);
ui.end_row();
ui.label("Date");
ui.add(DatePickerButton::new(&mut self.date));
ui.end_row();
});
}
}
impl Default for ProjectContext {
fn default() -> Self {
Self {
date: chrono::Local::now().naive_local().into(),
project_name: "New Project".to_string(),
project_author: "Your Name".to_string(),
project_description: "Description of your project".to_string(),
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
pub mod asset_editor; pub mod asset_editor;
pub mod content_editor; pub mod content_editor;
pub mod context_editor;
pub mod note_editor; pub mod note_editor;
pub mod object_editor; pub mod object_editor;
pub mod settings_editor;
pub mod tags; pub mod tags;
pub mod template_editor; pub mod template_editor;
+41 -16
View File
@@ -1,13 +1,19 @@
use std::fs; use std::path::Path;
use egui::TextEdit; use egui::TextEdit;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{PROJECT_FOLDER, editors::tags::Tag, util}; use crate::{
editors::tags::Tag,
filesystem::{FILESYSTEM, LegacyFileSystem},
util,
};
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Note { pub struct Note {
pub id: String,
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
pub content: String, pub content: String,
#[serde(default)] #[serde(default)]
@@ -17,12 +23,14 @@ pub struct Note {
pub tags: Vec<String>, pub tags: Vec<String>,
#[serde(skip)] #[serde(skip)]
pub id: String, #[serde(default = "default_saved")]
#[serde(skip)]
pub saved: bool, pub saved: bool,
} }
pub fn default_saved() -> bool {
true
}
impl Default for Note { impl Default for Note {
fn default() -> Self { fn default() -> Self {
Self { Self {
@@ -48,18 +56,15 @@ impl Note {
} }
} }
pub fn save(&mut self) -> std::io::Result<()> { pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let id = &self.id; let id = &self.id;
let path = PROJECT_FOLDER.join("notes").join(format!("{id}.json")); FILESYSTEM.write(Path::new(&format!("notes/{id}.json")), self.clone())?;
fs::write(path, serde_json::to_string(&self)?)?;
self.saved = true; self.saved = true;
Ok(()) Ok(())
} }
pub fn load(id: &str) -> std::io::Result<Self> { pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER.join("notes").join(format!("{id}.json")); let mut note: Self = FILESYSTEM.read(Path::new(&format!("notes/{id}.json")))?;
let content = fs::read_to_string(path)?;
let mut note: Note = serde_json::from_str(&content)?;
note.id = id.to_string(); note.id = id.to_string();
note.saved = true; note.saved = true;
Ok(note) Ok(note)
@@ -74,11 +79,31 @@ impl Note {
util::saved_status(ui, self.saved, &self.id, &self.name); util::saved_status(ui, self.saved, &self.id, &self.name);
if ui.button("Save").clicked() { ui.horizontal(|ui| {
if let Err(e) = self.save() { if ui.button("Save").clicked() {
eprintln!("Failed to save: {e}"); if let Err(e) = self.save() {
eprintln!("Failed to save: {e}");
}
} }
}
if ui.button("Create Copy").clicked() {
let new_id = uuid::Uuid::new_v4().to_string();
let mut new_note = self.clone();
new_note.id = new_id;
new_note.name = format!("{} (Copy)", self.name);
if let Err(e) = new_note.save() {
eprintln!("Failed to save copy: {e}");
}
}
if ui.button("Delete").clicked() {
if let Err(e) =
FILESYSTEM.delete(Path::new(&format!("notes/{id}.json", id = self.id)))
{
eprintln!("Failed to delete: {e}");
}
}
});
let id = ui.make_persistent_id("note_name"); let id = ui.make_persistent_id("note_name");
egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true) egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, true)
+10 -17
View File
@@ -1,6 +1,7 @@
use core::f32; use core::f32;
use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2}; use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::{ use crate::{
PROJECT_FOLDER, RightPanelContent, PROJECT_FOLDER, RightPanelContent,
@@ -8,6 +9,7 @@ use crate::{
tags::Tag, tags::Tag,
template_editor::{FieldValue, Template}, template_editor::{FieldValue, Template},
}, },
filesystem::{FILESYSTEM, LegacyFileSystem},
util, util,
}; };
@@ -81,21 +83,14 @@ impl ObjectInstance {
} }
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> { pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER let id = &self.id;
.join("objects") FILESYSTEM.write(Path::new(&format!("objects/{id}.json")), self.clone())?;
.join(format!("{}.json", &self.id));
let content = serde_json::to_string_pretty(self)?;
std::fs::write(&path, content)?;
self.saved = true; self.saved = true;
Ok(()) Ok(())
} }
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER.join("objects").join(format!("{id}.json")); let mut instance: Self = FILESYSTEM.read(Path::new(&format!("objects/{id}.json")))?;
let content = std::fs::read_to_string(&path)?;
let mut instance: ObjectInstance = serde_json::from_str(&content)?;
instance.saved = true; instance.saved = true;
Ok(instance) Ok(instance)
} }
@@ -137,12 +132,10 @@ impl ObjectInstance {
} }
if ui.button("Delete").clicked() { if ui.button("Delete").clicked() {
std::fs::remove_file( let id = &self.id;
PROJECT_FOLDER FILESYSTEM
.join("objects") .delete(Path::new(&format!("objects/{id}.json")))
.join(format!("{}.json", self.id)), .expect("Failed to delete object");
)
.unwrap();
*right_panel = Some(RightPanelContent::None); *right_panel = Some(RightPanelContent::None);
} }
@@ -274,7 +267,7 @@ impl ObjectInstance {
if !value.is_empty() { if !value.is_empty() {
let path = PROJECT_FOLDER.join("assets").join(&value); let path = PROJECT_FOLDER.join("assets").join(&value);
if let Ok(bytes) = std::fs::read(&path) { if let Ok(bytes) = FILESYSTEM.read_bytes(&path) {
let image_source = egui::ImageSource::Bytes { let image_source = egui::ImageSource::Bytes {
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()), uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
bytes: bytes.into(), bytes: bytes.into(),
+396
View File
@@ -0,0 +1,396 @@
use chrono::NaiveDate;
use egui::TextEdit;
use egui_extras::DatePickerButton;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use crate::{
filesystem::{FILESYSTEM, LegacyFileSystem},
util::saved_status,
};
#[derive(Serialize, Deserialize, Clone)]
pub struct ProjectSettings {
date: NaiveDate,
project_name: String,
project_author: String,
project_description: String,
// AI settings
#[cfg(feature = "llm")]
pub ai_context: String,
// settings
#[serde(skip)]
pub global_settings: EditorSettings,
#[serde(skip)]
pub local_overrides: EditorSettings,
#[serde(skip)]
pub open: bool,
#[serde(skip)]
pub saved: bool,
}
impl ProjectSettings {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Self {
if let Ok(mut proj) = FILESYSTEM.read::<Self>(Path::new("project.json")) {
proj.saved = true;
proj.global_settings = EditorSettings::load_global();
proj.local_overrides = EditorSettings::load();
proj
} else {
Self::default()
}
}
pub fn save(&mut self) {
FILESYSTEM
.write(Path::new("project.json"), self.clone())
.unwrap();
self.global_settings.save();
self.local_overrides.save();
self.saved = true;
}
#[allow(unused)]
fn config_str_override(
label: &str,
field: &mut Option<String>,
default: &str,
ui: &mut egui::Ui,
) -> bool {
let mut changed = false;
ui.label(label);
if let Some(value) = field {
if ui.text_edit_singleline(value).changed() {
changed = true;
};
if ui.button("Remove Override").clicked() {
*field = None;
changed = true;
}
} else if ui.button("Override").clicked() {
*field = Some(default.to_string());
changed = true;
}
ui.end_row();
changed
}
#[allow(unused)]
fn config_bool_override(
label: &str,
field: &mut Option<bool>,
default: bool,
ui: &mut egui::Ui,
) -> bool {
let mut changed = false;
ui.label(label);
if let Some(value) = field {
if ui.checkbox(value, "Enable AI").changed() {
changed = true;
};
if ui.button("Remove Override").clicked() {
*field = None;
changed = true;
}
} else if ui.button("Override").clicked() {
*field = Some(default);
changed = true;
}
ui.end_row();
changed
}
#[allow(unused)]
fn config_str(field: &mut String, label: &str, ui: &mut egui::Ui) -> bool {
let mut changed = false;
ui.label(label);
if ui.text_edit_singleline(field).changed() {
changed = true;
}
ui.end_row();
changed
}
#[allow(unused)]
fn config_bool(label: &str, field: &mut bool, ui: &mut egui::Ui) -> bool {
let mut changed = false;
ui.label(label);
if ui.checkbox(field, "Enable AI").changed() {
changed = true;
}
ui.end_row();
changed
}
#[allow(unused)]
pub fn ui(&mut self, ui: &mut egui::Ui) {
// save state
saved_status(ui, self.saved, "N/A", "Project Settings");
if ui.button("Save").clicked() {
self.save();
}
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
self.save();
}
ui.separator();
// project settings
ui.heading("Project Settings");
egui::Grid::new("project settings")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
if Self::config_str(&mut self.project_name, "Project Name", ui) { self.saved = false };
if Self::config_str(&mut self.project_author, "Project Author", ui) { self.saved = false };
if Self::config_str(&mut self.project_description, "Project Description", ui) { self.saved = false };
ui.label("Date");
if ui.add(DatePickerButton::new(&mut self.date)).changed() { self.saved = false };
ui.end_row();
#[cfg(feature = "llm")]
{
ui.label("AI Context Prompt");
if ui.add(TextEdit::multiline(&mut self.ai_context)
.font(egui::TextStyle::Monospace)
.interactive(true)
.frame(false)
.lock_focus(true)
.hint_text("What is this project about? what should the LLM know when generating content for this project?")).changed() { self.saved = false };
ui.end_row();
}
});
ui.separator();
// local settings overrides for editor
ui.heading("Local Overrides");
egui::Grid::new("local overrides")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
#[cfg(feature = "llm")]
if ProjectSettings::config_str_override(
"LLM API URI",
&mut self.local_overrides.llm_api_uri,
"http://localhost:1234",
ui,
) {
self.saved = false;
}
#[cfg(feature = "llm")]
if ProjectSettings::config_str_override(
"LLM API Key",
&mut self.local_overrides.llm_api_key,
"1234",
ui,
) {
self.saved = false;
}
#[cfg(feature = "llm")]
if ProjectSettings::config_bool_override(
"Enable AI",
&mut self.local_overrides.ai_enabled,
true,
ui,
) {
self.saved = false;
}
});
ui.separator();
// global editor settings
ui.heading("Global Editor Settings");
egui::Grid::new("global settings")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
#[cfg(feature = "llm")]
if Self::config_bool(
"Enable AI",
self.global_settings.ai_enabled.as_mut().unwrap(),
ui,
) {
self.saved = false;
}
#[cfg(feature = "llm")]
if Self::config_str(
self.global_settings.llm_api_uri.as_mut().unwrap(),
"LLM API URI",
ui,
) {
self.saved = false
};
#[cfg(feature = "llm")]
if Self::config_str(
self.global_settings.llm_api_key.as_mut().unwrap(),
"LLM API Key",
ui,
) {
self.saved = false
};
});
}
#[cfg(feature = "llm")]
#[allow(unused)]
pub fn ai_enabled(&mut self) -> bool {
let client = reqwest::blocking::Client::new();
if self.global_settings.ai_enabled.unwrap() {
return true;
}
if client
.get(self.global_settings.llm_api_uri.clone().unwrap() + "/v1/models")
.send()
.is_ok()
{
self.global_settings.ai_enabled = Some(true);
return true;
}
false
}
pub fn open(&mut self) {
self.open = true;
}
pub fn _close(&mut self) {
self.open = false;
}
}
impl Default for ProjectSettings {
fn default() -> Self {
Self {
date: chrono::Local::now().naive_local().into(),
project_name: "New Project".to_string(),
project_author: "Your Name".to_string(),
project_description: "Description of your project".to_string(),
#[cfg(feature = "llm")]
ai_context: "".to_string(),
global_settings: EditorSettings::default(),
local_overrides: EditorSettings::new(),
// window state
open: false,
saved: false,
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct EditorSettings {
pub dark_theme: Option<bool>,
#[cfg(feature = "llm")]
pub llm_api_uri: Option<String>,
#[cfg(feature = "llm")]
pub llm_api_key: Option<String>,
#[cfg(feature = "llm")]
pub ai_enabled: Option<bool>,
#[serde(skip)]
is_global: bool,
}
impl Default for EditorSettings {
fn default() -> Self {
Self {
#[cfg(feature = "llm")]
llm_api_uri: Some("http://localhost:1234".to_string()),
#[cfg(feature = "llm")]
llm_api_key: Some("".to_string()),
#[cfg(feature = "llm")]
ai_enabled: Some(true),
dark_theme: Some(true),
// window state
is_global: true,
}
}
}
impl EditorSettings {
pub fn new() -> Self {
Self {
#[cfg(feature = "llm")]
llm_api_uri: None,
#[cfg(feature = "llm")]
llm_api_key: None,
#[cfg(feature = "llm")]
ai_enabled: None,
dark_theme: None,
is_global: false,
}
}
pub fn load() -> Self {
if let Ok(mut settings) = FILESYSTEM.read::<Self>(Path::new("settings.json")) {
settings.is_global = false;
return settings;
}
Self::new()
}
pub fn save(&self) {
let path = if self.is_global {
FILESYSTEM.config_path()
} else {
PathBuf::from("settings.json")
};
FILESYSTEM.write(path.as_path(), self.clone()).unwrap()
}
pub fn load_global() -> Self {
let path = FILESYSTEM.config_path();
if !path.exists() {
FILESYSTEM.mkdir(path.parent().unwrap()).unwrap();
FILESYSTEM.write(path.as_path(), Self::default()).unwrap();
}
let mut settings = FILESYSTEM.read::<Self>(path.as_path()).unwrap();
settings.is_global = true;
settings
}
}
+11 -30
View File
@@ -1,12 +1,13 @@
use core::fmt;
use chrono::NaiveDate; use chrono::NaiveDate;
use core::fmt;
use egui::ScrollArea; use egui::ScrollArea;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::{ use crate::{
PROJECT_FOLDER, RightPanelContent, RightPanelContent,
editors::object_editor::ObjectInstance, editors::object_editor::ObjectInstance,
filesystem::{FILESYSTEM, LegacyFileSystem},
util::{self, Error}, util::{self, Error},
}; };
@@ -172,21 +173,14 @@ impl Default for Template {
impl Template { impl Template {
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER.join("templates").join(format!("{id}.json")); let mut template = FILESYSTEM.read::<Self>(Path::new(&format!("templates/{id}.json")))?;
let content = std::fs::read_to_string(&path)?;
let mut template: Self = serde_json::from_str(&content)?;
template.saved = true; template.saved = true;
Ok(template) Ok(template)
} }
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> { pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let path = PROJECT_FOLDER let id = &self.id;
.join("templates") FILESYSTEM.write(Path::new(&format!("templates/{id}.json")), self.clone())?;
.join(format!("{}.json", &self.id));
let content = serde_json::to_string_pretty(self)?;
std::fs::write(path, content)?;
self.saved = true; self.saved = true;
Ok(()) Ok(())
} }
@@ -204,16 +198,6 @@ impl Template {
ScrollArea::vertical().show(ui, |ui| { ScrollArea::vertical().show(ui, |ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
// ui.group(|ui| {
// ui.horizontal(|ui| {
// if self.saved {
// ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
// } else {
// ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
// }
// ui.label(format!("id: {}", self.id));
// });
// });
util::saved_status(ui, self.saved, &self.id, &self.name); util::saved_status(ui, self.saved, &self.id, &self.name);
// Save/Cancel buttons // Save/Cancel buttons
@@ -232,13 +216,10 @@ impl Template {
} }
if ui.button("Delete").clicked() { if ui.button("Delete").clicked() {
std::fs::remove_file( let id = &self.id;
PROJECT_FOLDER FILESYSTEM
.join("templates") .delete(Path::new(&format!("templates/{id}.json")))
.join(format!("{}.json", self.id)), .unwrap();
)
.unwrap();
*new_instance = Some(RightPanelContent::None); *new_instance = Some(RightPanelContent::None);
} }
+60 -106
View File
@@ -1,4 +1,4 @@
use walkdir::{DirEntry, WalkDir}; use std::path::Path;
use crate::{ use crate::{
PROJECT_FOLDER, RightPanelContent, PROJECT_FOLDER, RightPanelContent,
@@ -7,6 +7,7 @@ use crate::{
asset_editor::Asset, content_editor::ContentSection, object_editor::ObjectInstance, asset_editor::Asset, content_editor::ContentSection, object_editor::ObjectInstance,
tags::Tag, template_editor::Template, tags::Tag, template_editor::Template,
}, },
filesystem::{FILESYSTEM, FsError, LegacyFileSystem},
note_editor::Note, note_editor::Note,
}; };
@@ -162,14 +163,6 @@ impl Explorer {
}); });
} }
/// Recursively renders a tree of documents.
///
/// Each document is represented by a single element in the `documents` array.
/// The `parent_id` parameter is used to filter out documents that do not have the current
/// parent. If `parent_id` is `None`, all documents are rendered.
///
/// `load_doc` is a mutable reference to a `MainEditor`. When a document is clicked, it
/// is loaded into the `MainEditor` and returned as `Some`.
fn render_doc_branch( fn render_doc_branch(
ui: &mut egui::Ui, ui: &mut egui::Ui,
documents: &[ContentSection], documents: &[ContentSection],
@@ -238,100 +231,64 @@ impl Explorer {
} }
fn render_assets(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) { fn render_assets(&mut self, ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>) {
Self::render_asset_dir(ui, to_load, Path::new("assets"));
}
fn render_asset_dir(ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>, path: &Path) {
let files = FILESYSTEM.lsfiles(path).unwrap();
let dirs = FILESYSTEM.lsdirs(path).unwrap();
let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
egui::collapsing_header::CollapsingState::load_with_default_open( egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(), ui.ctx(),
ui.make_persistent_id("assets"), ui.make_persistent_id(&file_name),
true, false,
) )
.show_header(ui, |ui| { .show_header(ui, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("Assets"); ui.label(file_name);
let _clicked = ui.button("+").on_hover_text("Add new item").clicked();
}); });
}) })
.body(|ui| { .body(|ui| {
let entries: Vec<_> = WalkDir::new(PROJECT_FOLDER.join("assets")) // recursive call to render the next level of documents
.min_depth(1) for dir in dirs.iter() {
.max_depth(1) // Only immediate children Self::render_asset_dir(ui, to_load, dir);
.sort_by(|a, b| { }
// Directories first, then files
let a_is_dir = a.file_type().is_dir();
let b_is_dir = b.file_type().is_dir();
if a_is_dir == b_is_dir {
a.file_name().cmp(b.file_name())
} else if a_is_dir {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
})
.into_iter()
.filter_map(Result::ok)
.collect();
for entry in entries { for file in files.iter() {
Self::render_entry(ui, to_load, &entry); Self::render_asset(ui, to_load, file);
} }
}); });
} }
fn render_entry(ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>, entry: &DirEntry) { fn render_asset(ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>, path: &Path) {
let file_type = entry.file_type(); let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
let is_dir = file_type.is_dir();
let file_name = entry.file_name().to_string_lossy();
let path = entry.path();
if is_dir { if ui
let entries: Vec<_> = WalkDir::new(path) .selectable_label(false, format!("📄 {file_name}"))
.min_depth(1) .clicked()
.max_depth(1) {
.sort_by(|a, b| a.file_name().cmp(b.file_name())) // use asset::load to get the file at the path
.into_iter() let asset_path = path.strip_prefix(PROJECT_FOLDER.join("assets")).unwrap();
.filter_map(Result::ok) let asset = Asset::open(asset_path.to_string_lossy().to_string());
.collect(); *to_load = Some(RightPanelContent::Asset(Box::new(asset)));
egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
ui.make_persistent_id(&file_name),
false,
)
.show_header(ui, |ui| {
ui.horizontal(|ui| {
ui.label(file_name);
let _clicked = ui.button("+").on_hover_text("Add new item").clicked();
});
})
.body(|ui| {
// recursive call to render the next level of documents
for entry in entries {
Self::render_entry(ui, to_load, &entry);
}
});
} else {
// Handle file
if ui
.selectable_label(false, format!("📄 {file_name}"))
.clicked()
{
// use asset::load to get the file at the path
let asset_path = path.strip_prefix(PROJECT_FOLDER.join("assets")).unwrap();
let asset = Asset::open(asset_path.to_string_lossy().to_string());
*to_load = Some(RightPanelContent::Asset(Box::new(asset)));
}
} }
} }
// load templates from the templates folder // load templates from the templates folder
fn load_templates(&mut self) -> std::io::Result<()> { fn load_templates(&mut self) -> Result<(), FsError> {
let templates_folder = PROJECT_FOLDER.join("templates"); let path = Path::new("templates");
if !templates_folder.exists() {
std::fs::create_dir_all(&templates_folder)?; if !FILESYSTEM.exists(path) {
FILESYSTEM.mkdir(path)?;
} }
let mut templates = Vec::new(); let mut templates = Vec::new();
for entry in std::fs::read_dir(&templates_folder).unwrap() { for entry in FILESYSTEM.lsfiles(path)? {
let path = entry.unwrap().path(); match FILESYSTEM.read::<Template>(&entry) {
match Template::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(t) => templates.push(t), Ok(t) => templates.push(t),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"), Err(err) => eprintln!("Could not parse file {entry:?}: {err}"),
} }
} }
self.templates = templates; self.templates = templates;
@@ -340,17 +297,16 @@ impl Explorer {
} }
// load objects from the objects folder // load objects from the objects folder
fn load_objects(&mut self) -> std::io::Result<()> { fn load_objects(&mut self) -> Result<(), FsError> {
let objects_folder = PROJECT_FOLDER.join("objects"); let path = Path::new("objects");
if !objects_folder.exists() { if !FILESYSTEM.exists(path) {
std::fs::create_dir_all(&objects_folder)?; FILESYSTEM.mkdir(path)?;
} }
let mut objects = Vec::new(); let mut objects = Vec::new();
for entry in std::fs::read_dir(&objects_folder).unwrap() { for entry in FILESYSTEM.lsfiles(path)? {
let path = entry.unwrap().path(); match FILESYSTEM.read::<ObjectInstance>(&entry) {
match ObjectInstance::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(o) => objects.push(o), Ok(o) => objects.push(o),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"), Err(err) => eprintln!("Could not parse file {entry:?}: {err}"),
} }
} }
self.objects = objects; self.objects = objects;
@@ -359,16 +315,15 @@ impl Explorer {
} }
// load notes from the notes folder // load notes from the notes folder
fn load_notes(&mut self) -> std::io::Result<()> { fn load_notes(&mut self) -> Result<(), FsError> {
let notes_folder = PROJECT_FOLDER.join("notes"); let path = Path::new("notes");
if !notes_folder.exists() { if !FILESYSTEM.exists(path) {
std::fs::create_dir_all(&notes_folder)?; FILESYSTEM.mkdir(path)?;
} }
let mut notes = Vec::new(); let mut notes = Vec::new();
for entry in std::fs::read_dir(&notes_folder).unwrap() { for entry in FILESYSTEM.lsfiles(path)? {
let path = entry.unwrap().path(); match FILESYSTEM.read::<Note>(&entry) {
match Note::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(note) => notes.push(note), Ok(note) => notes.push(note),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"), Err(err) => eprintln!("Could not parse file {path:?}: {err}"),
} }
@@ -380,18 +335,17 @@ impl Explorer {
} }
// load documents from the documents folder // load documents from the documents folder
fn load_documents(&mut self) -> std::io::Result<()> { fn load_documents(&mut self) -> Result<(), FsError> {
let documents_folder = PROJECT_FOLDER.join("documents"); let path = Path::new("documents");
if !documents_folder.exists() { if !FILESYSTEM.exists(path) {
std::fs::create_dir_all(&documents_folder)?; FILESYSTEM.mkdir(path)?;
} }
let mut documents = Vec::new(); let mut documents = Vec::new();
for entry in std::fs::read_dir(&documents_folder).unwrap() { for entry in FILESYSTEM.lsfiles(path)? {
let path = entry.unwrap().path(); match FILESYSTEM.read::<ContentSection>(&entry) {
match ContentSection::load(path.file_stem().unwrap().to_str().unwrap()) {
Ok(document) => documents.push(MainEditor::open(document)), Ok(document) => documents.push(MainEditor::open(document)),
Err(err) => eprintln!("Could not parse file {path:?}: {err}"), Err(err) => eprintln!("Could not parse file {entry:?}: {err}"),
} }
} }
@@ -400,7 +354,7 @@ impl Explorer {
Ok(()) Ok(())
} }
fn load_tags(&mut self) -> std::io::Result<()> { fn load_tags(&mut self) -> Result<(), FsError> {
self.tags = Tag::load_all(); self.tags = Tag::load_all();
Ok(()) Ok(())
} }
+103
View File
@@ -0,0 +1,103 @@
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
sync::LazyLock,
};
use serde::{Serialize, de::DeserializeOwned};
#[cfg(feature = "native")]
use crate::PROJECT_FOLDER;
use crate::filesystem::native::NativeFileSystem;
#[cfg(feature = "native")]
pub mod native;
#[cfg(feature = "web")]
pub mod web;
pub static FILESYSTEM: LazyLock<NativeFileSystem> = LazyLock::new(|| {
#[cfg(feature = "native")]
return NativeFileSystem::new(PROJECT_FOLDER.clone());
#[cfg(feature = "web")]
return Box::new(web::WebFileSystem::new());
});
pub trait LegacyFileSystem {
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError>;
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError>;
fn write<T: Serialize>(&self, path: &Path, data: T) -> Result<(), FsError>;
fn delete(&self, path: &Path) -> Result<(), FsError>;
fn lsfiles(&self, path: &Path) -> Result<Vec<PathBuf>, FsError>;
fn lsdirs(&self, path: &Path) -> Result<Vec<PathBuf>, FsError>;
fn mkdir(&self, path: &Path) -> Result<(), FsError>;
fn rename(&self, path: &Path, new_path: &Path) -> Result<(), FsError>;
#[allow(unused)]
fn exists(&self, path: &Path) -> bool;
fn config_path(&self) -> PathBuf;
}
// ────────────────────────────────────────────────────────────────
// Custom error type
// ────────────────────────────────────────────────────────────────
#[derive(Debug)]
pub enum FsError {
Io(io::Error),
Serde(serde_json::Error),
}
impl std::fmt::Display for FsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FsError::Io(e) => write!(f, "IO error: {e}"),
FsError::Serde(e) => write!(f, "Serialization error: {e}"),
}
}
}
impl std::error::Error for FsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FsError::Io(e) => Some(e),
FsError::Serde(e) => Some(e),
}
}
}
// Convert the two underlying error types into our own
impl From<io::Error> for FsError {
fn from(err: io::Error) -> Self {
FsError::Io(err)
}
}
impl From<serde_json::Error> for FsError {
fn from(err: serde_json::Error) -> Self {
FsError::Serde(err)
}
}
#[allow(dead_code)]
pub struct Id(String);
#[allow(dead_code)]
pub trait FileSystem {
fn load<T: DeserializeOwned>(&self, id: Id) -> Result<T, FsError>;
fn save<T: Serialize>(&self, id: Id, data: T) -> Result<(), FsError>;
fn mkdir(&self, path: Path) -> Result<(), FsError>;
fn exists(&self, path: Path) -> bool;
}
#[allow(dead_code)]
pub struct Index {
file_cache: HashMap<Id, PathBuf>,
project_root: Directory,
}
#[allow(dead_code)]
pub struct Directory {
name: String,
id: Id,
children: HashMap<Id, Directory>,
files: Vec<Id>,
}
+116
View File
@@ -0,0 +1,116 @@
use std::fs;
use std::io::{ErrorKind, Read};
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::filesystem::{FsError, LegacyFileSystem};
pub struct NativeFileSystem {
project_root: PathBuf,
}
impl NativeFileSystem {
/// Create a new instance.
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
project_root: root.into(),
}
}
/// Resolve the user supplied *relative* path against the project root.
#[inline]
fn full_path(&self, path: &Path) -> PathBuf {
self.project_root.join(path)
}
}
impl LegacyFileSystem for NativeFileSystem {
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError> {
let full_path = self.full_path(path);
let file = fs::File::open(full_path).map_err(FsError::Io)?;
serde_json::from_reader(file).map_err(FsError::Serde)
}
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError> {
let full_path = self.full_path(path);
let mut contents = Vec::new();
fs::File::open(full_path)?.read_to_end(&mut contents)?;
Ok(contents)
}
fn write<T: Serialize>(&self, path: &Path, data: T) -> Result<(), FsError> {
let full_path = self.full_path(path);
// Ensure the parent directory exists.
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
let file = fs::File::create(full_path)?;
serde_json::to_writer(file, &data).map_err(FsError::Serde)
}
fn delete(&self, path: &Path) -> Result<(), FsError> {
let full_path = self.full_path(path);
match fs::remove_file(&full_path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::IsADirectory => {
// Remove a directory tree.
fs::remove_dir_all(full_path).map_err(FsError::Io)
}
Err(e) => Err(FsError::Io(e)),
}
}
fn mkdir(&self, path: &Path) -> Result<(), FsError> {
let full_path = self.full_path(path);
fs::create_dir_all(full_path).map_err(FsError::Io)
}
fn lsfiles(&self, path: &Path) -> Result<Vec<PathBuf>, FsError> {
let full_path = self.full_path(path);
let paths = fs::read_dir(full_path)?
.filter_map(|res| res.ok())
.filter(|entry| entry.file_type().unwrap().is_file())
.map(|entry| entry.path())
.collect();
Ok(paths)
}
fn lsdirs(&self, path: &Path) -> Result<Vec<PathBuf>, FsError> {
let full_path = self.full_path(path);
let paths = fs::read_dir(full_path)?
.filter_map(|res| res.ok())
.filter(|entry| entry.file_type().unwrap().is_dir())
.map(|entry| entry.path())
.collect();
Ok(paths)
}
fn exists(&self, path: &Path) -> bool {
let full_path = self.full_path(path);
full_path.exists()
}
fn rename(&self, path: &Path, other: &Path) -> Result<(), FsError> {
let full_path = self.full_path(path);
let full_other = self.full_path(other);
fs::rename(full_path, full_other).map_err(FsError::Io)
}
fn config_path(&self) -> PathBuf {
match std::env::var("HOME") {
Ok(path) => PathBuf::from(path + "/.config/worldcoder/settings.json"),
Err(_) => {
eprintln!(
"XDG_CONFIG_HOME not set, using default path of ~/.config/worldcoder/settings.json"
);
"~/.config/worldcoder/settings.json".into()
}
}
}
}
+490 -33
View File
@@ -1,58 +1,480 @@
use std::{
io::{BufRead, BufReader},
sync::{Arc, Mutex},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::editors::settings_editor::ProjectSettings;
#[derive(Clone)]
pub struct ContentAI {
pub open: bool,
// model input
pub content: String,
pub instruction: String,
pub context_override: String,
pub system_prompt: String,
// model settings
pub max_tokens: usize,
pub temperature: f32,
pub reasoning_effort: ReasoningEffort,
pub model_override: String,
// model output
pub reasoning: Arc<Mutex<String>>,
pub result: Arc<Mutex<String>>,
pub ready: Arc<Mutex<ReadyState>>,
}
impl ContentAI {
pub fn new(content: String) -> Self {
Self {
// model input
content,
instruction: String::new(),
context_override: String::new(),
system_prompt: String::new(),
// model settings
max_tokens: 2048,
reasoning_effort: ReasoningEffort::default(),
temperature: 0.7,
model_override: String::new(),
reasoning: Arc::new(Mutex::new(String::new())),
// output
result: Arc::new(Mutex::new(String::new())),
ready: Arc::new(Mutex::new(ReadyState::Idle)),
// ui
open: true,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
let is_open = self.open;
if is_open {
egui::SidePanel::right("ai_assistant").show_inside(ui, |ui| {
Self::ui_main(self, ui, project);
});
}
self.open = is_open;
}
fn ui_output_box(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
let mut ready_lock = self.ready.lock().unwrap();
ui.horizontal(|ui| {
if *ready_lock == ReadyState::Generating {
if ui.button("Cancel").clicked() {
*ready_lock = ReadyState::Halted;
}
if ui.button("Stop").clicked() {
*ready_lock = ReadyState::Idle;
}
ui.spinner();
ui.label("Generating...");
}
if *ready_lock == ReadyState::Idle {
let continue_content = || {
let content = self.content.clone();
let project = project.clone();
let result = self.result.clone();
let reasoning = self.reasoning.clone();
let ready = self.ready.clone();
let options = AIOptions {
max_completion_tokens: self.max_tokens,
reasoning_effort: self.reasoning_effort,
temperature: self.temperature,
model_override: if !self.model_override.is_empty() {
Some(self.model_override.clone())
} else {
None
},
};
let ai_input = AIInput {
system_prompt: self.system_prompt.clone(),
user_prompt: format!(
"{}\n\n{} {}",
self.instruction.clone(),
project.ai_context.clone(),
self.context_override.clone()
),
previous_content: content.clone(),
structure: None,
};
result.lock().unwrap().clear();
std::thread::spawn(move || {
let result = crate::llm_integration::content_llm::continue_content(
ai_input,
options,
project,
result,
reasoning,
ready.clone(),
);
if let Err(e) = result {
eprintln!("Error in content generation: {e}");
}
});
};
if ui.button("Generate ").clicked() {
continue_content();
}
ui.label("Idle");
}
// show regardless of state
if ui.button("Insert").clicked() {
*ready_lock = ReadyState::Ready;
}
if ui.button("Clear").clicked() {
self.result.lock().unwrap().clear();
self.reasoning.lock().unwrap().clear();
}
});
ui.spacing();
ui.vertical(|ui| {
egui::TopBottomPanel::top("reasoning_output")
.resizable(true)
.show_inside(ui, |ui| {
egui::ScrollArea::both()
.auto_shrink([false, true])
.id_salt("reasoning_output")
.max_width(ui.available_width())
// .max_height(ui.available_height() / 3.0)
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut *self.reasoning.lock().unwrap())
.font(egui::TextStyle::Monospace)
.interactive(false)
.desired_rows(5)
.frame(false)
.desired_width(ui.available_width())
.lock_focus(true)
.hint_text("Reasoning will appear here..."),
);
});
});
egui::ScrollArea::both()
.auto_shrink([false, false])
.id_salt("llm_output")
.max_width(ui.available_width())
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut *self.result.lock().unwrap())
.font(egui::TextStyle::Monospace)
.interactive(false)
.desired_rows(0)
.frame(false)
.desired_width(ui.available_width())
.lock_focus(true)
.hint_text("Content will appear here..."),
);
});
});
}
fn ui_main(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
{
ui.weak("(The model will see current file content)");
egui::CollapsingHeader::new("Settings")
.default_open(true)
.show(ui, |ui| {
egui::Grid::new("continue_grid")
.num_columns(2)
.striped(true)
.show(ui, |ui| {
ui.label("Max Tokens");
ui.add(
egui::DragValue::new(&mut self.max_tokens)
.range(128..=u32::MAX)
.speed(128),
);
ui.end_row();
ui.label("Temperature");
ui.add(
egui::DragValue::new(&mut self.temperature)
.range(0.0..=2.0)
.speed(0.1),
);
ui.label("Reasoning effort");
egui::ComboBox::from_id_salt("reasoning_effort")
.selected_text(self.reasoning_effort.to_string())
.show_ui(ui, |ui| {
ui.selectable_value(
&mut self.reasoning_effort,
ReasoningEffort::Minimal,
"Minimal",
);
ui.selectable_value(
&mut self.reasoning_effort,
ReasoningEffort::Low,
"Low",
);
ui.selectable_value(
&mut self.reasoning_effort,
ReasoningEffort::Medium,
"Medium",
);
ui.selectable_value(
&mut self.reasoning_effort,
ReasoningEffort::High,
"High",
);
});
ui.end_row();
ui.label("Model override");
ui.add(egui::TextEdit::singleline(&mut self.model_override));
ui.end_row();
});
});
egui::TopBottomPanel::top("continue_instruction")
.resizable(true)
.show_inside(ui, |ui| {
egui::CollapsingHeader::new("Instructions")
.default_open(true)
.show(ui, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.max_height(ui.available_height())
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut self.instruction)
.frame(false)
.desired_width(ui.available_width())
.hint_text("Writing Instructions"),
);
});
});
});
egui::TopBottomPanel::top("continue_context")
.resizable(true)
.show_inside(ui, |ui| {
egui::CollapsingHeader::new("Context")
.default_open(true)
.show(ui, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.max_height(ui.available_height())
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut self.context_override)
.frame(false)
.desired_width(ui.available_width())
.hint_text("Any additional context?"),
);
});
});
});
egui::TopBottomPanel::top("continue_system_prompt")
.resizable(true)
.show_inside(ui, |ui| {
egui::CollapsingHeader::new("System prompt")
.default_open(true)
.show(ui, |ui| {
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.max_height(ui.available_height())
.show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(&mut self.system_prompt)
.frame(false)
.desired_width(ui.available_width())
.hint_text("System prompt"),
);
});
});
});
self.ui_output_box(ui, project);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn continue_content( pub fn continue_content(
context: &str, ai_input: AIInput,
instruction: &str, // context: String,
_max_tokens: usize, // previous_content: String,
) -> Result<String, Box<dyn std::error::Error>> { // instruction: String,
options: AIOptions,
project: ProjectSettings,
result: Arc<Mutex<String>>,
reasoning: Arc<Mutex<String>>,
ready: Arc<Mutex<ReadyState>>,
) -> Result<(), Box<dyn std::error::Error>> {
*ready.lock().unwrap() = ReadyState::Generating;
let client = reqwest::blocking::Client::new(); let client = reqwest::blocking::Client::new();
let messages = vec![ let messages = vec![
Message { Message {
role: "system".to_string(), role: "system".to_string(),
content: " content: ai_input.system_prompt,
Please generate content that is a direct continuation of the given text.
Your response should be a logical next step in the content and should not repeat any of the text from the instruction or the content.
Do not generate any text that is not a direct continuation of the content.
if extra instructions are provided, follow them exactly, otherwise continue the text in a logical way.
".to_string(),
}, },
Message { Message {
role: "user".to_string(), role: "user".to_string(),
content: context.to_string(), content: format!(
}, "<Instructions> {}\n\n<Previous content> {}\n\n",
Message { ai_input.user_prompt, ai_input.previous_content
role: "user".to_string(), ),
content: format!("Instructions: {instruction}"),
}, },
]; ];
let request = ChatRequest { let request = ChatRequest {
messages, messages,
temperature: 0.7, temperature: options.temperature,
max_tokens: options.max_completion_tokens,
model: options.model_override,
reasoning_effort: options.reasoning_effort,
stream: true,
}; };
let response = client let llm_api_uri = if let Some(uri) = project.local_overrides.llm_api_uri {
.post("http://localhost:1234/v1/chat/completions") uri
.json(&request)
.send()?;
if !response.status().is_success() {
return Err(format!("Request failed: {}", response.text()?).into());
}
let response: ChatResponse = response.json()?;
if let Some(choice) = response.choices.into_iter().next() {
Ok(choice.message.content)
} else { } else {
Err("No response from model".into()) project.global_settings.llm_api_uri.unwrap()
};
let api_key = if let Some(key) = project.local_overrides.llm_api_key {
if key.is_empty() { None } else { Some(key) }
} else if let Some(key) = project.global_settings.llm_api_key {
if key.is_empty() { None } else { Some(key) }
} else {
return Err("No API key found".into());
};
let response = if let Some(k) = api_key {
client
.post(llm_api_uri + "/api/v0/chat/completions")
.json(&request)
.bearer_auth(k)
.send()?
} else {
client
.post(llm_api_uri + "/api/v0/chat/completions")
.json(&request)
.send()?
};
println!("success!");
// println!("response: {}", response.text().unwrap());
let reader = BufReader::new(response);
for line in reader.lines() {
// initial loop to check if the user has terminated the generation
{
let mut ready = ready.lock().unwrap();
if *ready == ReadyState::Halted {
result.lock().unwrap().clear();
reasoning.lock().unwrap().clear();
}
if *ready != ReadyState::Generating {
*ready = ReadyState::Idle;
break;
}
}
let line = line?;
if line == "data: [DONE]" {
break;
}
if let Some(json) = line.strip_prefix("data: ") {
if let Ok(chunk) = serde_json::from_str::<StreamingChatResponse>(json) {
println!("chunk: {chunk:?}");
if let Some(content) = chunk.choices[0].delta.content.as_ref() {
println!("content: {content}");
result.lock().unwrap().push_str(content);
}
if let Some(reasoning_content) = chunk.choices[0].delta.reasoning_content.as_ref() {
println!("reasoning_content: {reasoning_content}");
reasoning.lock().unwrap().push_str(reasoning_content);
}
}
}
} }
*ready.lock().unwrap() = ReadyState::Idle;
Ok(())
} }
pub fn ai_enabled() -> bool { pub struct AIOptions {
let client = reqwest::blocking::Client::new(); pub max_completion_tokens: usize,
client.get("http://localhost:1234/v1/models").send().is_ok() pub temperature: f32,
pub reasoning_effort: ReasoningEffort,
pub model_override: Option<String>,
}
pub struct AIInput {
pub system_prompt: String,
pub user_prompt: String,
pub previous_content: String,
#[allow(unused)]
pub structure: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ReadyState {
Idle,
Generating,
Ready,
Halted,
}
#[derive(Serialize, Copy, Clone, PartialEq, Default)]
pub enum ReasoningEffort {
#[serde(rename = "minimal")]
Minimal,
#[default]
#[serde(rename = "low")]
Low,
#[serde(rename = "medium")]
Medium,
#[serde(rename = "high")]
High,
}
impl std::fmt::Display for ReasoningEffort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReasoningEffort::Minimal => write!(f, "Minimal"),
ReasoningEffort::Low => write!(f, "Low"),
ReasoningEffort::Medium => write!(f, "Medium"),
ReasoningEffort::High => write!(f, "High"),
}
}
} }
// Simple request structure // Simple request structure
@@ -60,6 +482,39 @@ pub fn ai_enabled() -> bool {
struct ChatRequest { struct ChatRequest {
messages: Vec<Message>, messages: Vec<Message>,
temperature: f32, temperature: f32,
max_tokens: usize,
stream: bool,
reasoning_effort: ReasoningEffort,
// if we give the API model:null it returns 500
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
}
// Streaming response structures
#[derive(Deserialize, Debug)]
struct StreamingChatResponse {
choices: Vec<StreamingChoice>,
}
#[derive(Deserialize, Debug)]
struct StreamingChoice {
delta: Delta,
#[serde(default)]
#[allow(unused)]
finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
struct Delta {
#[serde(default)]
#[allow(unused)]
role: Option<String>,
#[serde(default)]
content: Option<String>,
#[serde(default)]
reasoning_content: Option<String>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@@ -70,10 +525,12 @@ struct Message {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct ChatResponse { struct ChatResponse {
#[allow(unused)]
choices: Vec<Choice>, choices: Vec<Choice>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct Choice { struct Choice {
#[allow(unused)]
message: Message, message: Message,
} }
+35 -18
View File
@@ -1,18 +1,23 @@
#![windows_subsystem = "windows"]
use std::{path::PathBuf, sync::LazyLock}; use std::{path::PathBuf, sync::LazyLock};
use egui::ScrollArea; use egui::ScrollArea;
mod editors; mod editors;
mod explorer; mod explorer;
#[cfg(feature = "llm")]
mod llm_integration; mod llm_integration;
mod scene;
mod util; mod util;
mod filesystem;
use crate::{ use crate::{
editors::{ editors::{
asset_editor::Asset, content_editor, context_editor::ProjectContext, note_editor, asset_editor::Asset, content_editor, note_editor, object_editor::ObjectInstance,
object_editor::ObjectInstance, tags::Tag, template_editor::Template, settings_editor::ProjectSettings, tags::Tag, template_editor::Template,
}, },
explorer::Explorer, explorer::Explorer,
}; };
@@ -31,16 +36,18 @@ fn main() {
..Default::default() ..Default::default()
}; };
let _ = eframe::run_native("Code Editor", options, Box::new(|_cc| Ok(Box::new(app)))); if let Err(e) = eframe::run_native("World Coder", options, Box::new(|_cc| Ok(Box::new(app)))) {
eprintln!("Failed to run app: {e}");
}
eprintln!("App closed");
} }
pub struct Interface { pub struct Interface {
// dialog: Option<egui_file::FileDialog>,
right_panel_content: RightPanelContent, right_panel_content: RightPanelContent,
editor: content_editor::MainEditor, editor: content_editor::MainEditor,
scene: scene::EditorScene,
explorer: Explorer, explorer: Explorer,
project: ProjectContext, project: ProjectSettings,
} }
impl eframe::App for Interface { impl eframe::App for Interface {
@@ -84,13 +91,12 @@ impl Interface {
Self { Self {
right_panel_content: RightPanelContent::None, right_panel_content: RightPanelContent::None,
editor: content_editor::MainEditor::new(), editor: content_editor::MainEditor::new(),
scene: scene::EditorScene::new(),
explorer: Explorer::new(), explorer: Explorer::new(),
project: ProjectContext::load(), project: ProjectSettings::load(),
} }
} }
fn render_top_panel(&self, ctx: &egui::Context) { fn render_top_panel(&mut self, ctx: &egui::Context) {
// Top bar with actions // Top bar with actions
egui::TopBottomPanel::top("top").show(ctx, |ui| { egui::TopBottomPanel::top("top").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -99,9 +105,24 @@ impl Interface {
ui.separator(); ui.separator();
// version // version
ui.label(VERSION) ui.label(VERSION);
// Settings
if ui.button("Settings").clicked() {
self.project.open();
}
}); });
}); });
if self.project.open {
let mut open = self.project.open;
egui::Window::new("Settings")
.open(&mut open)
.show(ctx, |ui| {
self.project.ui(ui);
});
self.project.open = open;
}
} }
fn render_left_panel(&mut self, ctx: &egui::Context) { fn render_left_panel(&mut self, ctx: &egui::Context) {
@@ -110,8 +131,7 @@ impl Interface {
.resizable(true) .resizable(true)
.default_width(250.0) .default_width(250.0)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.heading("Project Files"); ui.heading("Explorer");
ui.separator();
let mut to_load: Option<RightPanelContent> = None; let mut to_load: Option<RightPanelContent> = None;
let mut load_doc: Option<content_editor::MainEditor> = None; let mut load_doc: Option<content_editor::MainEditor> = None;
@@ -126,8 +146,6 @@ impl Interface {
if let Some(load_doc) = load_doc { if let Some(load_doc) = load_doc {
self.editor = load_doc; self.editor = load_doc;
self.editor.show_editor = true;
self.editor.show_preview = true;
} }
}); });
} }
@@ -188,8 +206,7 @@ impl Interface {
// render main content area // render main content area
fn render_main_content(&mut self, ctx: &egui::Context) { fn render_main_content(&mut self, ctx: &egui::Context) {
self.editor.ui(ctx); self.editor.ui(ctx, &mut self.project);
self.scene.ui(ctx, &mut self.explorer.objects());
} }
// configure appearance of UI elements // configure appearance of UI elements
@@ -214,7 +231,7 @@ impl Interface {
fonts.font_data.insert( fonts.font_data.insert(
"JetBrains Mono Nerd Font".to_string(), "JetBrains Mono Nerd Font".to_string(),
std::sync::Arc::new(egui::FontData::from_static(include_bytes!( std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
"/usr/local/share/fonts/j/JetBrainsMonoNerdFont_Regular.ttf", "../font/JetBrainsMonoNerdFontMono_Regular.ttf",
))), ))),
); );
-147
View File
@@ -1,147 +0,0 @@
use egui::{RichText, vec2};
use crate::{
PROJECT_FOLDER,
editors::{
object_editor::ObjectInstance,
template_editor::{FieldType, FieldValue, Template},
},
};
pub struct EditorScene {
rect: egui::Rect,
}
impl EditorScene {
pub fn new() -> Self {
Self {
rect: egui::Rect::ZERO,
}
}
pub fn ui(&mut self, ctx: &egui::Context, objects: &mut [ObjectInstance]) {
egui::CentralPanel::default()
.frame(egui::Frame::NONE)
.show(ctx, |ui| {
egui::Scene::default()
.zoom_range(0.1..=10.0)
.show(ui, &mut self.rect, |ui| {
ui.horizontal_wrapped(|ui| {
ui.set_max_width(5000.0);
// Group objects by their template_id
use std::collections::HashMap;
let mut objects_by_template: HashMap<String, Vec<&ObjectInstance>> =
HashMap::new();
for obj in objects {
objects_by_template
.entry(obj.template_id.clone())
.or_default()
.push(obj);
}
// For each template with objects, create cards
for (template_id, template_objects) in objects_by_template {
// Try to load the template to get field definitions
if let Ok(mut template) = Template::load(&template_id) {
for obj in template_objects {
// Create a card for each object
egui::Frame::group(ui.style())
.fill(egui::Color32::from_rgba_premultiplied(
30, 30, 30, 200,
))
.corner_radius(4.0)
.show(ui, |ui| {
ui.vertical(|ui| {
ui.set_max_width(512.0);
ui.set_min_width(512.0);
// Object name as header
ui.heading(RichText::new(&obj.name).strong());
// Show fields with on_preview = true
template.fields.sort_by_key(|field| field.field_type != FieldType::Image);
for field_def in &template.fields {
if field_def.on_preview {
if let Some(field_value) =
obj.fields.get(&field_def.name)
{
ui.separator();
match field_value {
FieldValue::SingleLine(
text,
) => {
ui.strong(&field_def.name);
ui.label(text);
}
FieldValue::MultiLine(
text,
) => {
ui.strong(&field_def.name);
ui.label(text);
}
FieldValue::Number(n) => {
ui.strong(&field_def.name);
ui.label(n.to_string());
}
FieldValue::Date(date) => {
ui.strong(&field_def.name);
ui.label(
date.format(
"%Y-%m-%d",
)
.to_string(),
);
}
FieldValue::Image(value) => {
if !value.is_empty() {
let path = PROJECT_FOLDER.join("assets").join(value);
if let Ok(bytes) = std::fs::read(&path) {
let image_source = egui::ImageSource::Bytes {
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
bytes: bytes.into(),
};
ui.add(
egui::Image::new(image_source).fit_to_exact_size(vec2(512.0, 512.0)),
);
}
}
}
FieldValue::Link(
target_id,
) => {
ui.strong(&field_def.name);
ui.label(format!(
"{target_id}"
));
}
FieldValue::Links(
links,
) => {
ui.strong(&field_def.name);
ui.label(format!(
"{} links",
links.len()
));
}
}
}
}
}
});
});
// Add some spacing between cards
ui.add_space(8.0);
}
}
}
});
});
});
}
}