1 Commits

Author SHA1 Message Date
91fd351d59 refactor: Router zusammenfuehren und offenen Workspace im Context fuehren
Die App hatte zwei Routable-Enums, zwischen denen ein Signal<bool> umschaltete.
Der Schalter wurde nie gesetzt - beim ersten Umlegen waere aufgefallen, dass
sich beide Router dieselbe History teilen und beide "/" belegen: Wer im
Selector auf "Open Workspace" klickt, steht auf /ws_open, und der
Editor-Router kennt nur / und /editor.

Jetzt ein Enum, der Editor-Teil unter #[nest("/workspace")]. An die Stelle des
bool tritt Signal<Option<Workspace>> in einem Context-Provider - Props
scheiden aus, weil sich durch ein Outlet keine durchreichen lassen. Fehlt der
Workspace, schickt ein Guard im EditorShell-Layout zurueck zum Selector; der
Fall tritt real bei Hot-Reload auf, wo der Context faellt und die Route bleibt.

Der Selector verliert seine Reiter und wird eine einzige Route. Das folgt aus
dem UI-Prototyp in Issue #8: Oeffnen ist der Alltagsfall, Hinzufuegen passiert
pro Workspace einmal - zwei gleichrangige Reiter verlangten bei jedem Start
eine Entscheidung, die fast immer dieselbe ist. Damit entfallen der leere
Stub create_new_workspace.rs, die Tab-Konstante fuer den Selector und die
zugehoerigen CSS-Klassen.

Weil EnumIter nicht mehr gebraucht wird, faellt strum_macros als direkte
Abhaengigkeit weg; strum re-exportiert die Derives bereits.

Entscheidungen dazu in den Issues #5, #6 und #8.
2026-08-28 08:57:19 +02:00
13 changed files with 143 additions and 367 deletions

1
Cargo.lock generated
View File

@@ -2330,7 +2330,6 @@ dependencies = [
"dioxus-code",
"dioxus-code-editor",
"strum",
"strum_macros",
]
[[package]]

View File

@@ -10,8 +10,7 @@ edition = "2021"
dioxus = { version = "0.7.1", features = ["router"] }
dioxus-code = "0.1.2"
dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] }
strum = { version = "0.28.0", features = ["strum_macros", "derive"] }
strum_macros = "0.28.0"
strum = { version = "0.28.0", features = ["derive"] }
[features]
default = ["desktop"]

View File

@@ -49,30 +49,6 @@
transform: scaleY(1);
}
/* --- Workspace selector tab ------------------------------------------------- */
.workspace-selector {
padding: var(--dn-space-2) var(--dn-space-4);
border-radius: var(--dn-radius);
color: var(--dn-fg-muted);
font-size: var(--dn-text-base);
font-weight: 500;
text-decoration: none;
white-space: nowrap;
transition: color var(--dn-dur-fast) var(--dn-ease),
background-color var(--dn-dur-fast) var(--dn-ease);
}
.workspace-selector:hover {
color: var(--dn-fg);
background: var(--dn-bg-raised);
}
.workspace-selector.active {
color: var(--dn-fg-on-accent);
background: var(--dn-accent);
}
/* --- Buttons ---------------------------------------------------------------- */
.btn {

View File

@@ -25,15 +25,6 @@
padding: var(--dn-space-6);
}
.workspace-selector-links {
display: flex;
gap: var(--dn-space-1);
padding: var(--dn-space-1);
background: var(--dn-bg-sunken);
border: 1px solid var(--dn-border);
border-radius: var(--dn-radius-lg);
}
.workspace-selector-view {
width: 100%;
max-width: 34rem;

View File

@@ -198,9 +198,18 @@
.flex {
display: flex;
}
.table {
display: table;
}
.border-collapse {
border-collapse: collapse;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.resize {
resize: both;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
@@ -208,6 +217,9 @@
.text-wrap {
text-wrap: wrap;
}
.underline {
text-decoration-line: underline;
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;

View File

@@ -1,236 +0,0 @@
<!doctype html>
<!--
PROTOTYP — WEGWERFCODE. Nicht in die App übernehmen.
Drei Varianten des Selector-Bildschirms, umschaltbar über ?variant=A|B|C,
dazu die Zustände über ?state=filled|empty|dialog.
Frage, die er beantwortet: Gitea-Issue #8 "Selector-UI: Anlegen und Öffnen".
Doppelklicken zum Öffnen. Bindet die echte CSS des Projekts ein, damit
Tokens und Komponenten nicht auseinanderlaufen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<title>DioNotes — Selector-Prototyp</title>
<link rel="stylesheet" href="assets/css/tokens.css">
<link rel="stylesheet" href="assets/css/base.css">
<link rel="stylesheet" href="assets/css/layout.css">
<link rel="stylesheet" href="assets/css/components.css">
<style>
/* ---- Nur für den Prototyp. Was hier steht, ist noch kein Projekt-CSS. ---- */
body { margin: 0; background: var(--dn-bg-app); color: var(--dn-fg);
font-family: var(--dn-font-ui); font-size: var(--dn-text-base); }
/* Listeneintrag — in allen Varianten gleich, damit die Varianten sich in der
Struktur unterscheiden und nicht in der Zeilengestaltung. */
.ws-item { display: flex; align-items: center; gap: var(--dn-space-3);
width: 100%; padding: var(--dn-space-3); border: 1px solid transparent;
border-radius: var(--dn-radius); background: transparent; color: inherit;
text-align: left; cursor: pointer; font: inherit;
transition: background-color var(--dn-dur-fast) var(--dn-ease); }
.ws-item:hover { background: var(--dn-bg-raised); }
.ws-item-body { min-width: 0; flex: 1; }
.ws-item-name { color: var(--dn-fg); font-weight: 500; }
.ws-item-path { color: var(--dn-fg-faint); font-family: var(--dn-font-code);
font-size: var(--dn-text-xs); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; direction: rtl; text-align: left; }
.ws-item.is-broken .ws-item-name { color: var(--dn-fg-muted); }
.ws-badge { flex: none; padding: 2px var(--dn-space-2);
border-radius: 999px; font-size: var(--dn-text-xs); white-space: nowrap; }
.ws-badge.unreachable { background: color-mix(in oklab, var(--dn-warning) 18%, transparent); color: var(--dn-warning); }
.ws-badge.nomarker { background: color-mix(in oklab, var(--dn-fg-faint) 22%, transparent); color: var(--dn-fg-muted); }
.ws-badge.damaged { background: color-mix(in oklab, var(--dn-danger) 18%, transparent); color: var(--dn-danger); }
.ws-list { display: flex; flex-direction: column; gap: 2px; }
/* Variante B */
.vb-shell { max-width: 40rem; margin: 0 auto; padding: var(--dn-space-7) var(--dn-space-5);
display: flex; flex-direction: column; gap: var(--dn-space-5); height: 100%;
box-sizing: border-box; }
.vb-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--dn-space-4); }
.vb-title { font-size: var(--dn-text-xl); font-weight: 600; }
.vb-list-wrap { flex: 1; min-height: 0; overflow-y: auto; }
.vb-foot { border-top: 1px solid var(--dn-border); padding-top: var(--dn-space-4);
display: flex; justify-content: space-between; align-items: center; gap: var(--dn-space-3); }
/* Variante C */
.vc-shell { display: grid; grid-template-columns: 1fr 20rem; height: 100%; }
.vc-main { padding: var(--dn-space-7); display: flex; flex-direction: column;
gap: var(--dn-space-4); min-height: 0; }
.vc-side { background: var(--dn-bg-sunken); border-left: 1px solid var(--dn-border);
padding: var(--dn-space-6) var(--dn-space-5); display: flex; flex-direction: column;
gap: var(--dn-space-5); }
.vc-brand { font-size: var(--dn-text-xl); font-weight: 600; }
.vc-tag { color: var(--dn-fg-faint); font-size: var(--dn-text-xs);
letter-spacing: var(--dn-tracking-caps); text-transform: uppercase; }
.vc-list-wrap { flex: 1; min-height: 0; overflow-y: auto; }
.vc-drop { border: 1px dashed var(--dn-border-strong); border-radius: var(--dn-radius-lg);
padding: var(--dn-space-5); text-align: center; color: var(--dn-fg-muted);
font-size: var(--dn-text-sm); }
@media (max-width: 720px) { .vc-shell { grid-template-columns: 1fr; }
.vc-side { border-left: 0; border-top: 1px solid var(--dn-border); } }
/* Dialog — in allen Varianten identisch, er ist selbst kein Variantenthema */
.pt-scrim { position: fixed; inset: 0; background: rgb(0 0 0 / 0.55);
display: grid; place-items: center; z-index: 50; }
.pt-dialog { width: min(28rem, calc(100vw - 2rem)); background: var(--dn-bg-sunken);
border: 1px solid var(--dn-border-strong); border-radius: var(--dn-radius-lg);
box-shadow: var(--dn-shadow); padding: var(--dn-space-5);
display: flex; flex-direction: column; gap: var(--dn-space-4); }
.pt-dialog h2 { margin: 0; font-size: var(--dn-text-md); }
.pt-dialog p { margin: 0; color: var(--dn-fg-muted); line-height: var(--dn-leading-normal); }
.pt-dialog code { font-family: var(--dn-font-code); font-size: var(--dn-text-sm);
color: var(--dn-fg); }
.pt-actions { display: flex; justify-content: flex-end; gap: var(--dn-space-2); }
/* Umschalter — bewusst hässlich, damit er nicht als Teil des Entwurfs gelesen wird */
.pt-bar { position: fixed; bottom: var(--dn-space-4); left: 50%;
transform: translateX(-50%); z-index: 99; display: flex; align-items: center;
gap: var(--dn-space-1); padding: var(--dn-space-1); border-radius: 999px;
background: #000; border: 1px solid #555; box-shadow: 0 6px 24px rgb(0 0 0 / .5);
font-family: var(--dn-font-code); font-size: var(--dn-text-xs); }
.pt-bar button { border: 0; background: transparent; color: #fff; cursor: pointer;
padding: 6px 10px; border-radius: 999px; font: inherit; }
.pt-bar button:hover { background: #333; }
.pt-bar .pt-label { padding: 0 var(--dn-space-3); color: #fff; white-space: nowrap; }
.pt-bar .pt-sep { width: 1px; align-self: stretch; background: #444; margin: 0 4px; }
.pt-bar button.on { background: #fff; color: #000; }
</style>
</head>
<body>
<div id="main"><div id="stage"></div></div>
<div class="pt-bar">
<button id="prev" title="Vorherige Variante (←)">◀</button>
<span class="pt-label" id="label"></span>
<button id="next" title="Nächste Variante (→)">▶</button>
<span class="pt-sep"></span>
<button data-state="filled">Liste</button>
<button data-state="empty">Erststart</button>
<button data-state="dialog">Rückfrage</button>
</div>
<script>
const WS = [
{ name: "Notizen", path: "C:\\Users\\marvin\\Notizen" },
{ name: "DioNotes-Testdaten", path: "D:\\Projekte\\DioNotes-Testdaten" },
{ name: "Archiv 2024", path: "E:\\Backup\\Archiv 2024", badge: "unreachable", text: "nicht erreichbar" },
{ name: "Ideen", path: "C:\\Users\\marvin\\Ideen", badge: "nomarker", text: "kein Workspace mehr" },
{ name: "Rezepte", path: "C:\\Users\\marvin\\Rezepte",badge: "damaged", text: "beschädigt" },
];
const esc = s => s.replace(/[&<>"]/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;" }[c]));
const item = w => `
<button class="ws-item ${w.badge ? "is-broken" : ""}">
<span class="ws-item-body">
<span class="ws-item-name">${esc(w.name)}</span>
<span class="ws-item-path">${esc(w.path)}</span>
</span>
${w.badge ? `<span class="ws-badge ${w.badge}">${esc(w.text)}</span>` : ""}
</button>`;
const list = () => `<div class="ws-list">${WS.map(item).join("")}</div>`;
const dialog = `
<div class="pt-scrim"><div class="pt-dialog">
<h2>Ordner ist nicht leer</h2>
<p><code>D:\\Notizen-Import</code> enthält bereits 34 Dateien. DioNotes legt darin
nur den Ordner <code>.dionotes/</code> an und verändert nichts Vorhandenes.</p>
<div class="pt-actions">
<button class="btn btn-ghost">Abbrechen</button>
<button class="btn btn-primary">Workspace anlegen</button>
</div>
</div></div>`;
/* ---- Variante A — Reiter, wie der heutige Code es anlegt ---------------- */
const A = state => `
<div class="workspace-selector-layout">
<div class="workspace-selector-links">
<a class="workspace-selector" href="#">Create new Workspace</a>
<a class="workspace-selector active" href="#">Open Workspace</a>
</div>
<div class="workspace-selector-view">
${state === "empty" ? `
<div class="empty-state">
<span class="empty-state-title">Noch kein Workspace</span>
<span>Lege einen an, um loszulegen.</span>
<button class="btn btn-primary">Ordner wählen…</button>
</div>` : list()}
</div>
</div>`;
/* ---- Variante B — eine Ansicht, Liste führt, Anlegen ist Nebenaktion ---- */
const B = state => `
<div class="vb-shell">
<div class="vb-head">
<span class="vb-title">Workspace öffnen</span>
<span class="field-hint">${state === "empty" ? "" : WS.length + " bekannt"}</span>
</div>
<div class="vb-list-wrap">
${state === "empty" ? `
<div class="empty-state">
<span class="empty-state-title">Hier ist noch nichts</span>
<span>Der erste Workspace ist ein Ordner deiner Wahl.</span>
</div>` : list()}
</div>
<div class="vb-foot">
<span class="field-hint">Ein Workspace ist ein Ordner mit Notizen.</span>
<button class="btn ${state === "empty" ? "btn-primary" : ""}">Neuen anlegen…</button>
</div>
</div>`;
/* ---- Variante C — Startbildschirm, zwei Spalten ------------------------- */
const C = state => `
<div class="vc-shell">
<div class="vc-main">
<span class="vc-tag">Zuletzt verwendet</span>
<div class="vc-list-wrap">
${state === "empty" ? `
<div class="vc-drop">Noch keine Workspaces.<br>Rechts einen Ordner wählen.</div>`
: list()}
</div>
</div>
<aside class="vc-side">
<div>
<div class="vc-brand">DioNotes</div>
<span class="field-hint">Markdown-Notizen, lokal.</span>
</div>
<div class="field">
<span class="field-label">Neuer Workspace</span>
<button class="btn btn-primary">Ordner wählen…</button>
<span class="field-hint">Legt <code>.dionotes/</code> im gewählten Ordner an.</span>
</div>
</aside>
</div>`;
const V = { A: [A, "Reiter (heutiger Code)"], B: [B, "Liste führt"], C: [C, "Startbildschirm"] };
const keys = Object.keys(V);
const p = new URLSearchParams(location.search);
let variant = keys.includes(p.get("variant")) ? p.get("variant") : "A";
let state = ["filled","empty","dialog"].includes(p.get("state")) ? p.get("state") : "filled";
function render() {
const [fn, name] = V[variant];
document.getElementById("stage").innerHTML =
fn(state === "dialog" ? "filled" : state) + (state === "dialog" ? dialog : "");
document.getElementById("label").textContent = `${variant} — ${name}`;
document.querySelectorAll("[data-state]").forEach(b =>
b.classList.toggle("on", b.dataset.state === state));
history.replaceState(null, "", `?variant=${variant}&state=${state}`);
}
const cycle = d => { variant = keys[(keys.indexOf(variant) + d + keys.length) % keys.length]; render(); };
document.getElementById("prev").onclick = () => cycle(-1);
document.getElementById("next").onclick = () => cycle(1);
document.querySelectorAll("[data-state]").forEach(b =>
b.onclick = () => { state = b.dataset.state; render(); });
addEventListener("keydown", e => {
if (/^(INPUT|TEXTAREA)$/.test(e.target.tagName) || e.target.isContentEditable) return;
if (e.key === "ArrowLeft") cycle(-1);
if (e.key === "ArrowRight") cycle(1);
});
render();
</script>
</body>
</html>

View File

@@ -1,9 +1,12 @@
mod views;
mod components;
mod route;
mod views;
mod workspace;
use dioxus::prelude::*;
use crate::views::editor::dionotes_editor::EditorRoute;
use crate::views::workspace_selector::selector::WorkspaceRoute;
use crate::route::Route;
use crate::workspace::Workspace;
const FAVICON: Asset = asset!("/assets/favicon.ico");
@@ -22,7 +25,11 @@ fn main() {
#[component]
fn App() -> Element {
let workspace_selected: Signal<bool> = use_signal(|| false);
// Der offene Workspace als Context, nicht als Prop: Er wird im Selector
// gesetzt und in der Datei-Leiste gelesen — beide hängen unter einem
// Outlet, durch das sich keine Props durchreichen lassen.
use_context_provider(|| Signal::new(None::<Workspace>));
rsx! {
document::Link { rel: "icon", href: FAVICON }
// Tailwind first: its preflight lives in @layer base, our unlayered
@@ -32,12 +39,6 @@ fn App() -> Element {
document::Link { rel: "stylesheet", href: BASE_CSS }
document::Link { rel: "stylesheet", href: LAYOUT_CSS }
document::Link { rel: "stylesheet", href: COMPONENTS_CSS }
if workspace_selected() {
Router::<EditorRoute> {}
} else {
Router::<WorkspaceRoute> {}
}
Router::<Route> {}
}
}

36
src/route.rs Normal file
View File

@@ -0,0 +1,36 @@
use dioxus::prelude::*;
use strum::EnumMessage;
use crate::views::editor::dionotes_editor::{Editor, EditorShell, Home};
use crate::views::workspace_selector::selector::Selector;
/// Alle Routen der App in einem Enum.
///
/// Bewusst *ein* Router statt zweier: Zwei Routable-Enums teilen sich dieselbe
/// History, und beide hatten `/` belegt — beim Umschalten hätte der Editor eine
/// URL zu sehen bekommen, die er nicht kennt.
#[derive(Debug, Clone, Routable, PartialEq, EnumMessage)]
#[rustfmt::skip]
pub enum Route {
#[nest("/workspace")]
#[layout(EditorShell)]
#[route("/")]
#[strum(message = "Home")]
Home {},
#[route("/editor")]
#[strum(message = "Editor")]
Editor {},
#[end_layout]
#[end_nest]
#[route("/")]
Selector {},
}
impl Route {
/// Die Einträge der Datei-Leiste im Editor.
///
/// Explizit statt über `EnumIter`, weil das Enum jetzt auch den Selector
/// enthält und der in dieser Leiste nichts zu suchen hat. Wermutstropfen:
/// eine neue Editor-Route hier einzutragen erzwingt der Compiler nicht.
pub const EDITOR_TABS: [Self; 2] = [Self::Home {}, Self::Editor {}];
}

View File

@@ -1,32 +1,24 @@
use dioxus::prelude::*;
use dioxus_code::Theme;
use dioxus_code_editor::{CodeEditor, Language};
use strum::EnumMessage;
use crate::components::custom_link::custom_link_component::CustomLink;
use strum::{EnumMessage, IntoEnumIterator};
use strum_macros::EnumIter;
use crate::route::Route;
use crate::workspace::Workspace;
const HEADER_SVG: Asset = asset!("/assets/header.svg");
#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)]
#[rustfmt::skip]
pub enum EditorRoute {
#[layout(Navbar)]
#[route("/")]
#[strum(message = "Home")]
Home {},
#[route("/editor")]
#[strum(message = "Editor")]
Editor {},
}
/// Home page
#[component]
fn Home() -> Element {
pub fn Home() -> Element {
rsx! {
Hero {}
}
}
// TODO: Unveränderter Dioxus-Vorlagentext. Ersetzen, sobald klar ist, was
// beim Öffnen eines Workspace angezeigt wird, bevor eine Notiz gewählt wurde.
#[component]
pub fn Hero() -> Element {
rsx! {
@@ -46,7 +38,7 @@ pub fn Hero() -> Element {
}
#[component]
fn Editor() -> Element {
pub fn Editor() -> Element {
let mut source = use_signal(String::new);
rsx! {
@@ -60,15 +52,34 @@ fn Editor() -> Element {
}
}
/// Shared navbar component.
/// Die Editor-Hülle: Datei-Leiste links, Editorfläche rechts.
///
/// Trägt zugleich den Zustands-Guard — ohne offenen Workspace hat der Editor
/// nichts anzuzeigen. Der Fall ist real: Hot-Reload setzt den Context zurück,
/// während die Route bestehen bleibt.
#[component]
fn Navbar() -> Element {
pub fn EditorShell() -> Element {
let workspace = use_context::<Signal<Option<Workspace>>>();
let navigator = use_navigator();
use_effect(move || {
if workspace.read().is_none() {
navigator.replace(Route::Selector {});
}
});
if workspace.read().is_none() {
return rsx! {};
}
rsx! {
div {
class: "layout-container",
div {
class: "files-bar",
for entry in EditorRoute::iter() {
// TODO: Durch den Notiz-Baum des offenen Workspace ersetzen
// (rekursiv eingelesen, aufklappbar). Bis dahin die Routen.
for entry in Route::EDITOR_TABS {
CustomLink {
key: "{entry}",
label: entry.get_message().unwrap_or_default(),
@@ -79,8 +90,8 @@ fn Navbar() -> Element {
}
div {
class: "editor-container",
Outlet::<EditorRoute> { }
Outlet::<Route> { }
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
use dioxus::prelude::*;
/// Create new Workspace Layout
// TODO: Platzhalter — Formular zum Anlegen eines Workspace (Name, Pfad) und
// Verdrahtung als Ziel von WorkspaceRoute::NewWorkspace fehlen noch.
#[component]
fn NewWorkspaceLayout() -> Element {
rsx! {
div {}
}
}

View File

@@ -1,2 +1 @@
pub mod selector;
pub mod create_new_workspace;

View File

@@ -1,61 +1,26 @@
use dioxus::prelude::*;
use strum::{EnumMessage, IntoEnumIterator};
use strum_macros::EnumIter;
use crate::components::custom_link::custom_link_component::CustomLink;
#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)]
#[rustfmt::skip]
pub enum WorkspaceRoute {
#[layout(Workspace)]
#[route("/")]
#[strum(message = "Create new Workspace")]
NewWorkspace {},
#[route("/ws_open")]
#[strum(message = "Open Workspace")]
OpenWorkspace {},
}
/// Page to create new Workspace
/// Der Bildschirm vor dem Editor: einen bekannten Workspace öffnen oder einen
/// hinzufügen. Eine einzige Route, keine Reiter — die Workspace-Liste führt,
/// das Hinzufügen ist Nebenaktion.
#[component]
fn NewWorkspace() -> Element {
pub fn Selector() -> Element {
rsx! {
div {
"New Workspace",
}
}
}
/// Page to open available Workspaces
#[component]
fn OpenWorkspace() -> Element {
rsx! {
div {
"Open Workspace",
}
}
}
/// Workspace Layout
#[component]
fn Workspace() -> Element {
rsx! {
div {
class: "workspace-selector-layout",
div {
class: "workspace-selector-links",
for route in WorkspaceRoute::iter() {
CustomLink {
key: "{route}",
label: route.get_message().unwrap_or_default(),
href: route.clone(),
css: "workspace-selector",
div { class: "workspace-selector-layout",
div { class: "workspace-selector-view",
// TODO: Workspace-Liste aus workspaces.ron laden und darstellen
// (Ordnername, Pfad darunter, Statusabzeichen). Bis dahin nur
// der leere Zustand.
div { class: "empty-state",
span { class: "empty-state-title", "Noch kein Workspace" }
span { "Der erste Workspace ist ein Ordner deiner Wahl." }
// TODO: Nativen Ordner-Dialog anbinden, Marker anlegen und
// den Workspace in den Context schreiben.
button { class: "btn btn-primary", disabled: true,
"Workspace hinzufügen…"
}
}
}
div {
class: "workspace-selector-view",
Outlet::<WorkspaceRoute> { }
}
}
}
}

34
src/workspace.rs Normal file
View File

@@ -0,0 +1,34 @@
use std::path::PathBuf;
/// Ein geöffneter Workspace — der Ordner auf der Platte, in dem die Notizen liegen.
///
/// Heute genau ein Feld. Der Typ existiert trotzdem als eigener Name, weil
/// `CONTEXT.md` „Workspace" als Domänenbegriff führt und die Einstellungen aus
/// `.dionotes/workspace.ron` hier andocken werden.
#[derive(Debug, Clone, PartialEq)]
pub struct Workspace {
pub root: PathBuf,
}
impl Workspace {
/// Anzeigename: der Ordnername. Bewusst abgeleitet statt gespeichert —
/// ein eigenes Namensfeld wäre nach dem ersten Umbenennen des Ordners falsch.
pub fn name(&self) -> String {
self.root
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| self.root.to_string_lossy().into_owned())
}
}
#[cfg(test)]
mod tests {
use super::Workspace;
use std::path::PathBuf;
#[test]
fn name_is_the_folder_name() {
let ws = Workspace { root: PathBuf::from("C:/Users/marvin/Notizen") };
assert_eq!(ws.name(), "Notizen");
}
}