From 7fa669164515340295c9a1bd54a7d97fd4b1e5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delorme?= Date: Tue, 19 May 2026 00:04:17 +0200 Subject: [PATCH 1/3] upated plan for req-68 --- .../reqs/req-68-git-client-and-connector.md | 168 +++++++++++++----- 1 file changed, 124 insertions(+), 44 deletions(-) diff --git a/src/docs/reqs/req-68-git-client-and-connector.md b/src/docs/reqs/req-68-git-client-and-connector.md index 4caa57b..a3c948d 100644 --- a/src/docs/reqs/req-68-git-client-and-connector.md +++ b/src/docs/reqs/req-68-git-client-and-connector.md @@ -52,14 +52,14 @@ Immediately after a successful `git init` + identity setup, a confirmation dialo The first time the user clicks **↑ Push** (or selects `↑ Push` from the context menu) when no remote is configured, an **Add Remote** dialog appears: -| Field | Description | -|-------|-------------| -| Remote URL | HTTPS or SSH URL (e.g. `https://github.com/user/repo.git` or `git@github.com:user/repo.git`) | -| Authentication | Selector: **None / Basic / Token / SSH key** | -| Username | Visible for Basic and Token auth | -| Password / Token | Visible for Basic and Token auth; masked input | -| SSH private key path | Visible for SSH auth; file-picker | -| SSH passphrase | Visible for SSH auth; optional, masked | +| Field | Description | +|----------------------|----------------------------------------------------------------------------------------------| +| Remote URL | HTTPS or SSH URL (e.g. `https://github.com/user/repo.git` or `git@github.com:user/repo.git`) | +| Authentication | Selector: **None / Basic / Token / SSH key** | +| Username | Visible for Basic and Token auth | +| Password / Token | Visible for Basic and Token auth; masked input | +| SSH private key path | Visible for SSH auth; file-picker | +| SSH passphrase | Visible for SSH auth; optional, masked | Credentials are stored in `~/.marknote/config` (plain-text, owner-readable only, `chmod 600`). The remote is registered as `origin` in the local repo config. After the remote is saved the push is retried automatically. @@ -71,11 +71,11 @@ Credentials are stored in `~/.marknote/config` (plain-text, owner-readable only, Three buttons are prepended to the existing toolbar and are only visible when the open project is a Git repository: -| Button | Shortcut | Action | -|--------|----------|--------| -| `↓ Pull` | — | Fetch + fast-forward merge from `origin/` | -| `↑ Push` | — | Push local commits to `origin/`; prompts Add Remote if none set | -| `✓ Commit` | — | Opens the Commit dialog | +| Button | Shortcut | Action | +|------------|----------|-------------------------------------------------------------------------| +| `↓ Pull` | — | Fetch + fast-forward merge from `origin/` | +| `↑ Push` | — | Push local commits to `origin/`; prompts Add Remote if none set | +| `✓ Commit` | — | Opens the Commit dialog | A read-only **branch badge** (dark pill, e.g. `⎇ main`) appears at the right end of the toolbar, showing the current branch name. It is refreshed whenever a commit, pull, or push completes. @@ -83,33 +83,33 @@ A read-only **branch badge** (dark pill, e.g. `⎇ main`) appears at the right e Right-clicking any file in the tree appends a **Git** section (preceded by a separator) to the existing file context menu: -| Entry | Condition | Action | -|-------|-----------|--------| -| `+ git add` | File is untracked or modified | Stages the file (`git add `) | -| `✓ Commit…` | Always | Opens Commit dialog with this file pre-selected | -| `↓ Pull` | Repo has a remote | Pull from origin | -| `↑ Push` | Repo has a remote | Push to origin | -| `↺ Fetch` | Repo has a remote | Fetch from origin (no merge) | -| `✗ Remove from index` | File is tracked | Unstages / removes from index (`git rm --cached`) | +| Entry | Condition | Action | +|-----------------------|-------------------------------|---------------------------------------------------| +| `+ git add` | File is untracked or modified | Stages the file (`git add `) | +| `✓ Commit…` | Always | Opens Commit dialog with this file pre-selected | +| `↓ Pull` | Repo has a remote | Pull from origin | +| `↑ Push` | Repo has a remote | Push to origin | +| `↺ Fetch` | Repo has a remote | Fetch from origin (no merge) | +| `✗ Remove from index` | File is tracked | Unstages / removes from index (`git rm --cached`) | ### Context menu — root folder Right-clicking the **root folder** node shows the same Git section as for files, plus two entries at the top when the project is not yet a Git repository: -| Entry | Condition | -|-------|-----------| -| `⚙ Initialize Git repository…` | No `.git/` present | -| `⊕ Add Remote…` | Repo exists but has no `origin` remote | +| Entry | Condition | +|--------------------------------|----------------------------------------| +| `⚙ Initialize Git repository…` | No `.git/` present | +| `⊕ Add Remote…` | Repo exists but has no `origin` remote | ### File status indicators Status dots are rendered as small coloured circles on the right edge of each tree row (as currently): -| Colour | Meaning | -|--------|---------| -| Green | Tracked and up to date (clean) | +| Colour | Meaning | +|--------|--------------------------------------| +| Green | Tracked and up to date (clean) | | Orange | Tracked but locally modified (dirty) | -| Red | Untracked (not in the index) | +| Red | Untracked (not in the index) | The dots are refreshed asynchronously after every Git operation and after every file save. @@ -272,12 +272,12 @@ A dedicated **Git** tab is added to the Options dialog (`Help → Options… → A radio-button selector controls which Git controls are displayed in the Project Explorer toolbar: -| Mode | Description | -|------|-------------| -| **Standard** | Only the existing `⇅ Sync` and `↻ Index` buttons are shown. The branch badge is displayed but is read-only (label only, no interaction). This is the default mode; it is appropriate for users who do not need direct Git interaction from within MarkNote. | +| Mode | Description | +|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Standard** | Only the existing `⇅ Sync` and `↻ Index` buttons are shown. The branch badge is displayed but is read-only (label only, no interaction). This is the default mode; it is appropriate for users who do not need direct Git interaction from within MarkNote. | | **Advanced** | All Git buttons are active: `↓ Pull`, `↑ Push`, `✓ Commit`, `↻ Index`, `⇅ Sync`, and the `⎇ ` badge. Selecting the branch badge opens a read-only dropdown listing existing local branches; switching branches is intentionally not supported (no branching operations in scope). | -> [!IMPORTANT] IMPORTANT +> [!IMPORTANT] IMPORTANT > Even in **Standard** mode the per-file Git status dots (green / orange / red) and the root-folder context-menu entries (Init, Add Remote…) remain visible and functional. The mode only governs the toolbar buttons. The selected mode is persisted in `~/.marknote/config` under the key `git.toolbarMode` with values `standard` (default) or `advanced`. @@ -309,17 +309,97 @@ No additional native binaries are required. JGit bundles all transitive dependen ## ToDo -- [x] Use plan mode to prepare the implementation, -- [x] wait for plan approval before proceeding, -- [x] write a dedicated specification in `src/docs/git-client-and-connector-implementation.md`, -- [x] add JGit dependency to `pom.xml`, -- [x] implement `GitService` with all operations listed above, -- [x] update Project Explorer toolbar (Pull / Push / Commit buttons + branch badge), -- [x] add Git section to file and root-folder context menus, -- [x] implement Commit dialog, -- [x] implement Add Remote / Credentials dialog, -- [x] wire status dot refresh to post-operation events, -- [x] add Git tab in Options dialog (toolbar mode: standard / advanced; remote credentials). +1. Stage 1 - GitService + + - [x] Use plan mode to prepare the implementation, + - [x] wait for plan approval before proceeding, + - [x] write a dedicated specification in `src/docs/git-client-and-connector-implementation.md`, + - [x] add JGit dependency to `pom.xml`, + - [x] implement `GitService` with all operations listed above, + - [x] update Project Explorer toolbar (Pull / Push / Commit buttons + branch badge), + - [x] add Git section to file and root-folder context menus, + - [x] implement Commit dialog, + - [x] implement Add Remote / Credentials dialog, + - [x] wire status dot refresh to post-operation events, + - [x] add Git tab in Options dialog (toolbar mode: standard / advanced; remote credentials). + +2. Stage 2 - git RemoteConnector + + **Goal**: Implement connectors for interacting with GitHub, GitLab, and Gitea REST APIs to list repositories and create new remote repositories directly from MarkNote. + + **Architecture decisions**: + - Use existing `java.net.http.*` HttpClient (already used in UpdateChecker) + - Parse JSON responses with `org.json.simple` (already in dependencies for LLM config) + - Token-based authentication only (credentials already stored in AppConfig) + - Return empty lists on API errors, log via LogService + - Static factory for platform detection from remote URLs + + **Phase 1: Core Infrastructure** (parallel steps) + + - [ ] Create `RemoteConnector` interface in `src/main/java/utils/RemoteConnector.java` + - Define methods: `platform()`, `listRepositories()`, `createRepository(String name, boolean isPrivate)` + - Define `RemoteRepo` record: `name`, `cloneUrl`, `description`, `isPrivate`, `defaultBranch` + - Add JavaDoc describing contract and authentication expectations + + - [ ] Create `RemoteConnectorFactory` utility in `src/main/java/utils/RemoteConnectorFactory.java` + - Static method `create(String remoteUrl, String token)` → returns appropriate connector or null + - URL parsing logic to detect platform (github.com, gitlab.com, custom domains) + - Support for custom GitLab and Gitea instances + + **Phase 2: Platform Implementations** (parallel steps) + + - [ ] Implement `GitHubConnector` in `src/main/java/utils/GitHubConnector.java` + - Constructor: `GitHubConnector(String token)` + - API base: `https://api.github.com` + - `listRepositories()`: GET `/user/repos?type=owner&per_page=100` + - `createRepository()`: POST `/user/repos` with JSON body `{"name": "...", "private": true/false}` + - Auth header: `Authorization: token ` + + - [ ] Implement `GitLabConnector` in `src/main/java/utils/GitLabConnector.java` + - Constructor: `GitLabConnector(String instanceUrl, String token)` + - Support custom instances (default: `https://gitlab.com`) + - API base: `/api/v4` + - `listRepositories()`: GET `/projects?owned=true&per_page=100` + - `createRepository()`: POST `/projects` with JSON body `{"name": "...", "visibility": "private"/"public"}` + - Auth header: `PRIVATE-TOKEN: ` + + - [ ] Implement `GiteaConnector` in `src/main/java/utils/GiteaConnector.java` + - Constructor: `GiteaConnector(String instanceUrl, String token)` + - Support custom instances only (no default, user provides URL) + - API base: `/api/v1` + - `listRepositories()`: GET `/user/repos` + - `createRepository()`: POST `/user/repos` with JSON body `{"name": "...", "private": true/false}` + - Auth header: `Authorization: token ` + + **Phase 3: Testing & Error Handling** + + - [ ] Create unit tests in `src/test/java/utils/` + - `RemoteConnectorFactoryTest`: URL parsing, platform detection + - Mock HTTP responses for connector tests + - Test authentication header generation + - Test JSON parsing for repository lists + + - [ ] Add error handling and logging + - Wrap HTTP exceptions with user-friendly messages + - Log API errors via `LogService` with source "RemoteConnector" + - Handle common errors: 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Rate Limited + - Return empty lists on error (consistent with GitService async patterns) + + **Verification steps**: + - Manual test: Create connector instances with valid tokens, verify `listRepositories()` returns expected repos + - Manual test: Call `createRepository("test-repo", true)` on each platform, verify repo created + - Unit tests: `mvn test -Dtest=RemoteConnectorFactoryTest` + - Integration check: Factory correctly identifies platforms from various URL formats + + **Design decisions**: + - **JSON library**: Use `org.json.simple` (already in classpath) + - **Custom domains**: Auto-detect gitlab.com, require explicit URL for self-hosted instances + - **Error handling**: Return empty lists + log errors (no UI exceptions) + - **Factory pattern**: Static utility (no caching, connectors are lightweight) + - **Scope**: Repository operations only; no issues, PRs, CI status (deferred to future) + +3. stage 3 - UI integration + - [ ] integrate the RemoteConnector usage into git dialogs and UI. > [!IMPORTANT] GitService class > The Git client is a big feature; create it as a dedicated `GitService` class and keep all JGit calls inside it. The UI layer must never import JGit directly. From 6dba0b6160576d99465038fadb7597fa94e0dd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delorme?= Date: Tue, 19 May 2026 00:13:48 +0200 Subject: [PATCH 2/3] feat: Implement connectors for GitHub, GitLab, and Gitea APIs - Added GitHubConnector for interacting with GitHub's REST API, including methods for listing and creating repositories. - Added GitLabConnector for interacting with GitLab's REST API, supporting both public and self-hosted instances. - Added GiteaConnector for interacting with Gitea's REST API, specifically for self-hosted instances. - Created RemoteConnector interface to define common operations for all connectors. - Implemented RemoteConnectorException for error handling across connectors. - Developed RemoteConnectorFactory to instantiate the appropriate connector based on the provided repository URL. - Added unit tests for RemoteConnectorException, RemoteConnectorFactory, and RemoteRepo to ensure functionality and error handling. --- docs/index.md | 2 +- {src/docs => docs}/tips-and-tricks-en.md | 0 .../reqs/req-68-git-client-and-connector.md | 79 +- src/docs/user-guide-en.md | 1604 ----------------- src/main/java/utils/GitHubConnector.java | 194 ++ src/main/java/utils/GitLabConnector.java | 215 +++ src/main/java/utils/GiteaConnector.java | 196 ++ src/main/java/utils/RemoteConnector.java | 85 + .../java/utils/RemoteConnectorException.java | 105 ++ .../java/utils/RemoteConnectorFactory.java | 132 ++ .../utils/RemoteConnectorExceptionTest.java | 101 ++ .../utils/RemoteConnectorFactoryTest.java | 105 ++ src/test/java/utils/RemoteRepoTest.java | 157 ++ 13 files changed, 1336 insertions(+), 1639 deletions(-) rename {src/docs => docs}/tips-and-tricks-en.md (100%) delete mode 100644 src/docs/user-guide-en.md create mode 100644 src/main/java/utils/GitHubConnector.java create mode 100644 src/main/java/utils/GitLabConnector.java create mode 100644 src/main/java/utils/GiteaConnector.java create mode 100644 src/main/java/utils/RemoteConnector.java create mode 100644 src/main/java/utils/RemoteConnectorException.java create mode 100644 src/main/java/utils/RemoteConnectorFactory.java create mode 100644 src/test/java/utils/RemoteConnectorExceptionTest.java create mode 100644 src/test/java/utils/RemoteConnectorFactoryTest.java create mode 100644 src/test/java/utils/RemoteRepoTest.java diff --git a/docs/index.md b/docs/index.md index 58c67ae..bbf3774 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,8 @@ # MakNote Wiki - [User Documentation](user-guide.md) +- [Tips & Tricks](tips-and-tricks-en.md) # Build & Release - [Creata a package](create-packages.md) - diff --git a/src/docs/tips-and-tricks-en.md b/docs/tips-and-tricks-en.md similarity index 100% rename from src/docs/tips-and-tricks-en.md rename to docs/tips-and-tricks-en.md diff --git a/src/docs/reqs/req-68-git-client-and-connector.md b/src/docs/reqs/req-68-git-client-and-connector.md index a3c948d..ea202ce 100644 --- a/src/docs/reqs/req-68-git-client-and-connector.md +++ b/src/docs/reqs/req-68-git-client-and-connector.md @@ -336,67 +336,78 @@ No additional native binaries are required. JGit bundles all transitive dependen **Phase 1: Core Infrastructure** (parallel steps) - - [ ] Create `RemoteConnector` interface in `src/main/java/utils/RemoteConnector.java` + - [x] Create `RemoteConnector` interface in `src/main/java/utils/RemoteConnector.java` - Define methods: `platform()`, `listRepositories()`, `createRepository(String name, boolean isPrivate)` - Define `RemoteRepo` record: `name`, `cloneUrl`, `description`, `isPrivate`, `defaultBranch` - Add JavaDoc describing contract and authentication expectations - - [ ] Create `RemoteConnectorFactory` utility in `src/main/java/utils/RemoteConnectorFactory.java` + - [x] Create `RemoteConnectorException` class in `src/main/java/utils/RemoteConnectorException.java` + - Checked exception with HTTP status code, API message, and cause + - User-friendly error messages for common HTTP codes (401, 403, 404, 429, 500+) + + - [x] Create `RemoteConnectorFactory` utility in `src/main/java/utils/RemoteConnectorFactory.java` - Static method `create(String remoteUrl, String token)` → returns appropriate connector or null - URL parsing logic to detect platform (github.com, gitlab.com, custom domains) - Support for custom GitLab and Gitea instances + - Uses JGit's URIish for parsing HTTPS and SSH URLs **Phase 2: Platform Implementations** (parallel steps) - - [ ] Implement `GitHubConnector` in `src/main/java/utils/GitHubConnector.java` + - [x] Implement `GitHubConnector` in `src/main/java/utils/GitHubConnector.java` - Constructor: `GitHubConnector(String token)` - API base: `https://api.github.com` - - `listRepositories()`: GET `/user/repos?type=owner&per_page=100` - - `createRepository()`: POST `/user/repos` with JSON body `{"name": "...", "private": true/false}` - - Auth header: `Authorization: token ` + - `listRepositories()`: GET `/user/repos?type=owner&per_page=100&sort=updated` + - `createRepository()`: POST `/user/repos` with JSON body `{"name": "...", "private": true/false, "auto_init": true}` + - Auth header: `Authorization: token `, API version: `X-GitHub-Api-Version: 2022-11-28` + - Uses java.net.http.HttpClient with 10s connection timeout - - [ ] Implement `GitLabConnector` in `src/main/java/utils/GitLabConnector.java` - - Constructor: `GitLabConnector(String instanceUrl, String token)` + - [x] Implement `GitLabConnector` in `src/main/java/utils/GitLabConnector.java` + - Constructors: `GitLabConnector(String token)` for gitlab.com, `GitLabConnector(String instanceUrl, String token)` for custom - Support custom instances (default: `https://gitlab.com`) - API base: `/api/v4` - - `listRepositories()`: GET `/projects?owned=true&per_page=100` - - `createRepository()`: POST `/projects` with JSON body `{"name": "...", "visibility": "private"/"public"}` + - `listRepositories()`: GET `/projects?owned=true&per_page=100&order_by=updated_at&sort=desc` + - `createRepository()`: POST `/projects` with JSON body `{"name": "...", "visibility": "private"/"public", "initialize_with_readme": true}` - Auth header: `PRIVATE-TOKEN: ` - - [ ] Implement `GiteaConnector` in `src/main/java/utils/GiteaConnector.java` + - [x] Implement `GiteaConnector` in `src/main/java/utils/GiteaConnector.java` - Constructor: `GiteaConnector(String instanceUrl, String token)` - Support custom instances only (no default, user provides URL) - API base: `/api/v1` - - `listRepositories()`: GET `/user/repos` - - `createRepository()`: POST `/user/repos` with JSON body `{"name": "...", "private": true/false}` + - `listRepositories()`: GET `/user/repos?limit=100` + - `createRepository()`: POST `/user/repos` with JSON body `{"name": "...", "private": true/false, "auto_init": true, "default_branch": "main"}` - Auth header: `Authorization: token ` **Phase 3: Testing & Error Handling** - - [ ] Create unit tests in `src/test/java/utils/` - - `RemoteConnectorFactoryTest`: URL parsing, platform detection - - Mock HTTP responses for connector tests - - Test authentication header generation - - Test JSON parsing for repository lists + - [x] Create unit tests in `src/test/java/utils/` + - `RemoteConnectorFactoryTest`: 14 tests for URL parsing, platform detection, connector instantiation + - `RemoteConnectorExceptionTest`: 11 tests for exception construction, status codes, error messages + - `RemoteRepoTest`: 11 tests for record validation, null handling, default values + - Total: 36 tests, all passing - - [ ] Add error handling and logging - - Wrap HTTP exceptions with user-friendly messages - - Log API errors via `LogService` with source "RemoteConnector" - - Handle common errors: 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Rate Limited - - Return empty lists on error (consistent with GitService async patterns) + - [x] Add error handling and logging + - All HTTP exceptions wrapped in `RemoteConnectorException` with detailed context + - Log API errors via `LogService` with sources "GitHubConnector", "GitLabConnector", "GiteaConnector" + - Handle common errors: 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Rate Limited, 500+ Server errors + - Throw `RemoteConnectorException` on errors (user-friendly messages for UI display) + - Extract error messages from JSON responses (GitHub: "message", GitLab: "message" or "error") **Verification steps**: - - Manual test: Create connector instances with valid tokens, verify `listRepositories()` returns expected repos - - Manual test: Call `createRepository("test-repo", true)` on each platform, verify repo created - - Unit tests: `mvn test -Dtest=RemoteConnectorFactoryTest` - - Integration check: Factory correctly identifies platforms from various URL formats - - **Design decisions**: - - **JSON library**: Use `org.json.simple` (already in classpath) - - **Custom domains**: Auto-detect gitlab.com, require explicit URL for self-hosted instances - - **Error handling**: Return empty lists + log errors (no UI exceptions) - - **Factory pattern**: Static utility (no caching, connectors are lightweight) - - **Scope**: Repository operations only; no issues, PRs, CI status (deferred to future) + - [x] Unit tests: `mvn test -Dtest=RemoteConnectorFactoryTest,RemoteConnectorExceptionTest,RemoteRepoTest` → 36 tests passed + - [x] Compilation: `mvn clean compile -DskipTests` → BUILD SUCCESS (no warnings) + - [x] Factory validation: Correctly identifies github.com, gitlab.com, custom GitLab, and Gitea from HTTPS/SSH URLs + - [x] Error handling: All connectors throw RemoteConnectorException with HTTP status codes and API messages + + **Design decisions** (confirmed during implementation): + - **JSON library**: `org.json.simple` (already in classpath for LLM config) + - **HTTP client**: `java.net.http.HttpClient` with 10s timeout (consistent with UpdateChecker pattern) + - **Custom domains**: Auto-detect gitlab.com; for self-hosted GitLab/Gitea, extract base URL from remote URL + - **Error handling**: Throw `RemoteConnectorException` with HTTP status + API message (UI can display user-friendly errors) + - **Factory pattern**: Static utility with `create()` and `detectPlatform()` methods (no caching, lightweight connectors) + - **GitLab detection**: Check for "gitlab.com" first, then "gitlab" substring for self-hosted instances + - **Default branch**: Normalize to "main" if null/blank in RemoteRepo record + - **Authentication**: Token-based only (stored in AppConfig, chmod 600) + - **Scope**: Repository operations only; no issues, PRs, CI status (deferred to future Stage 4+) 3. stage 3 - UI integration - [ ] integrate the RemoteConnector usage into git dialogs and UI. diff --git a/src/docs/user-guide-en.md b/src/docs/user-guide-en.md deleted file mode 100644 index b57f1e6..0000000 --- a/src/docs/user-guide-en.md +++ /dev/null @@ -1,1604 +0,0 @@ ---- -title: "MarkNote User Guide" -date: 2026-03-10 -version: "0.1.5" -author: "Frédéric Delorme" -description: "Official user guide for MarkNote, a lightweight Markdown editor built with JavaFX." -summary: "Welcome to MarkNote, a lightweight and modern Markdown editor built with JavaFX. This guide will help you get started and make the most of MarkNote's features." -tags: [marknote, markdown, user-guide, documentation] -lang: en -status: draft ---- - -# MarkNote User Guide - -Version 0.1.5 - -Welcome to MarkNote, a lightweight and modern Markdown editor built with JavaFX. This guide will help you get started and make the most of MarkNote's features. - -## Table of Contents - -1. [Introduction](#introduction) -2. [Getting Started](#getting-started) -3. [Main Interface](#main-interface) -4. [Working with Documents](#working-with-documents) -5. [Search & Replace in Editor](#search--replace-in-editor) -6. [Editor Context Menu & Floating Toolbar](#editor-context-menu--floating-toolbar) -7. [Front Matter Panel](#front-matter-panel) -8. [Project Explorer](#project-explorer) -9. [Git Support](#git-support) -10. [Search & Indexing](#search--indexing) -11. [Tag Cloud](#tag-cloud) -12. [Network Diagram](#network-diagram) -13. [Status Bar](#status-bar) -14. [Live Preview](#live-preview) -15. [LLM Chat](#llm-chat) -16. [Splash Screen & About](#splash-screen--about) -17. [Themes](#themes) -18. [Options & Settings](#options--settings) -19. [Keyboard Shortcuts](#keyboard-shortcuts) -20. [Troubleshooting](#troubleshooting) - ---- - -## Introduction - -MarkNote is a cross-platform Markdown editor designed for writers, developers, and anyone who works with Markdown documents. It provides a distraction-free writing environment with real-time preview, project management, and customizable themes. - -### Key Features - -- **Markdown Editing** - Full-featured editor with syntax highlighting (headings, bold, italic, strikethrough, code, blockquotes, lists, links, images, horizontal rules) -- **Live Preview** - Real-time HTML rendering as you type -- **Syntax Highlighting** - Code blocks with automatic language detection and theme-coordinated coloring -- **Code Block Copy Button** - One-click copy for code blocks in preview -- **Markdown Tables** - Full GFM table support with styled rendering -- **Task Lists** - GitHub-style checkboxes (`[ ]` / `[x]`) rendered in preview -- **GitHub Alerts** - Styled blockquotes for `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]` -- **PlantUML Diagrams** - Render PlantUML diagrams directly in the preview; switch between the **online PlantUML server** (default) or a **local `plantuml.jar`** configured in Options → Tools; local rendering is asynchronous (per-block background threads) and shows a ⚙ spinning gear icon in the status bar during generation; **in-memory SVG cache** avoids regenerating unchanged diagrams -- **Mermaid Diagrams** - Render Mermaid flowcharts, sequences, and more in the preview (theme auto-matches app theme) -- **Math Equations** - LaTeX/MathML support via KaTeX (`$...$` inline, `$$...$$` block) -- **Front Matter Panel** - Collapsible panel above the editor showing and editing YAML front matter metadata, with UUID-based document linking via drag & drop -- **Project Explorer** - Browse and manage your project files, with front matter titles displayed for `.md` files -- **Project Indexing** - Automatic incremental indexing of Markdown files by front matter and filenames -- **Search** - Instant full-text search across indexed documents with live results popup (up to 20 results) -- **Search & Replace in Editor** - In-editor overlay bar (`Ctrl+F` / `Ctrl+H`) with Regex, Full Word, and Match Case toggles, occurrence navigation and bulk replace -- **LLM Chat** - Optional dockable assistant panel with streaming responses, conversation history, system context, export, and insertion of generated content into the active document -- **Tag Cloud** - Visual tag cloud showing tag frequency; click to search -- **Network Diagram** - Interactive force-directed graph of document links and shared tags, with tooltips and current document highlighting -- **Status Bar** - Document info, statistics, and indexing progress at the bottom of the window -- **Multi-document Tabs** - Work on multiple files simultaneously, with drag-to-reorder tabs -- **Panel Detachment** - Detach side panels to independent tabs, and restore them back to their docked position -- **Theme Support** - Built-in themes with custom theme creation and a full CSS theme editor -- **Splash Screen** - Themed splash screen at startup (configurable) -- **Image Preview** - Quick preview for images with zoom/pan and format/size info overlay -- **Recent Projects** - Quick access to your recent work (files and projects, with Clear History) -- **Project Session Restore** - Open documents are automatically saved when you close a project and restored when you reopen it; the session is stored in a `.marknote` file at the project root -- **Scroll Synchronization** - The editor and preview panels scroll in sync so that the rendered output always reflects the text at the cursor position -- **Drag & Drop** - Drop files into the editor to insert Markdown links or images -- **Reading Mode** - Distraction-free fullscreen reading with the preview panel filling the screen; the Project Explorer floats as a compact overlay with a minimize toggle; all other panels are hidden and fully restored on exit (`Ctrl+Shift+P`) -- **Multi-language Support** - Available in 5 languages - ---- - -## Getting Started - -### First Launch - -When you first launch MarkNote, you'll see the **Splash Screen** displaying the application name, version, author, and copyright. Click anywhere or wait a few seconds to dismiss it. - -Then you'll be greeted with the **Welcome page**: - -![Welcome Page](illustrations/welcome-page.svg) - -The Welcome page shows: - -- A list of your recent projects (if any) -- Quick access buttons to open projects - -### Opening a Project - -1. Click **File → Open project...** in the menu -2. Navigate to your project folder -3. Click **Open** - -Your project files will appear in the Project Explorer panel. - -### Creating a New Document - -1. Click **File → New doc** or press `Ctrl+N` -2. A new untitled document tab will open -3. Start writing your Markdown content - ---- - -## Main Interface - -MarkNote's interface is divided into three main areas: - -![Main Interface](illustrations/main-interface.svg) - -### 1. Project Explorer, Tag Cloud & Network Diagram (Left Panel) - -The left panel contains three sub-panels arranged vertically in a resizable split: - -- **Project Explorer** (top) - Displays your project's file structure in a tree view. Navigate through folders, double-click files to open them, and right-click for context menu options. -- **Tag Cloud** (middle) - Shows all tags found in your project's Markdown front matter, with font size proportional to frequency. Click a tag to search for it. -- **Network Diagram** (bottom) - An interactive force-directed graph showing the relationships between documents (via links) and shared tags. See [Network Diagram](#network-diagram) for details. - -### 2. Search Box (Top Bar) - -Located to the right of the menu bar, the search box lets you instantly search across all indexed documents. Results appear in a popup as you type. - -### 2b. Edit Menu - -The **Edit** menu provides in-document text operations: - -| Item | Shortcut | Description | -|------|----------|-------------| -| **Search...** | `Ctrl+F` | Open the Search bar overlay (search field + options) | -| **Search and Replace...** | `Ctrl+H` | Open the Search & Replace overlay (both fields) | - -### 3. Editor (Center Panel) - -The main editing area where you write your Markdown. Features include: - -- Syntax highlighting for Markdown elements (headings, bold, italic, strikethrough, code, blockquotes, lists, links, images, horizontal rules) -- Tab-based interface for multiple documents -- Drag-to-reorder tabs within the tab bar -- Undo/Redo support -- Line numbers (optional) -- Tab names truncated to 15 characters with ellipsis (full name in tooltip) -- Modified documents prefixed with `*` in the tab title - -### 3a. Front Matter Panel (Above Editor) - -Above the editor text area, a **collapsible Front Matter panel** displays and lets you edit the YAML front matter of the current document. See [Front Matter Panel](#front-matter-panel) for details. - -![Front Matter Panel](illustrations/front-matter-panel.svg) - -### 4. Preview Panel (Right Panel) - -Shows the rendered HTML output of your Markdown in real-time. Features: - -- Navigation buttons (back/forward through history) -- Refresh button -- Clickable links that navigate within your project - -### 4b. LLM Chat Panel (Right Dock) - -When enabled in **Help → Options... → LLM**, MarkNote adds an **LLM Chat** panel on the right side of the workspace. - -It provides: - -- A conversation view with your prompts and streamed assistant responses -- A prompt input area with a **System Context** button, submit button, and cancel button while a request is running -- Quick actions to copy, export, edit, or insert generated content into the active document - -See [LLM Chat](#llm-chat) for setup and usage details. - -### 5. Status Bar (Bottom) - -A thin bar at the bottom of the window showing: - -- **Document name** and **cursor position** (line:column) on the left -- **Statistics** (document count, line count, word count) in the center -- **PlantUML local-jar indicator** (right of center, visible only when local PlantUML mode is active): - - **⚙ spinning gear** — animated during diagram rendering via the local jar - - **● PlantUML: local jar** — static badge confirming local mode is on -- **Indexing progress bar** on the right (visible only during indexing) - -### Toggling Panels - -You can show or hide panels using the **View** menu or by clicking the **×** close button on each panel header: - -![View Menu](illustrations/view-menu.svg) - -| Shortcut | Action | -|----------|--------| -| **View → Project explorer** (`Ctrl+E`) | Toggle the left Project Explorer | -| **View → Preview panel** (`Ctrl+P`) | Toggle the right Preview pane | -| **View → Tag Cloud** (`Ctrl+T`) | Toggle the Tag Cloud sub-panel | -| **View → Network Diagram** (`Ctrl+L`) | Toggle the Network Diagram sub-panel | -| **View → LLM Chat** (`Ctrl+M`) | Toggle the LLM Chat panel when the feature is enabled | -| **View → Enter Reading Mode** (`Ctrl+Shift+P`) | Enter distraction-free fullscreen reading mode | -| **View → Show Welcome** | Open the Welcome tab | - -Each panel can also be closed by clicking the **×** button in its header. Re-opening it from the View menu restores it to the layout. - -### Detaching Panels to Tabs - -Left-side panels (**Project Explorer**, **Tag Cloud**, **Network Diagram**) plus the **Preview** and **LLM Chat** panels can be **detached** from their docked position and converted into independent tabs in the main editor area. This gives you more flexibility to organize your workspace. - -**To detach a panel:** - -1. Click the **⇱ detach button** (window icon) in the panel's header bar -2. The panel disappears from its docked position and opens as a new tab in the editor tab bar -3. The View menu checkbox for that panel is automatically unchecked - -**To restore a detached panel:** - -1. Click the **⇲ restore button** (dock icon) in the tab's header, OR -2. Right-click on the tab and select **Restore to Panel**, OR -3. Re-enable the panel from the **View** menu (this will close the tab and restore the panel to its original position) - -![Panel Detach](illustrations/panel-detach.svg) - -> **Tip:** Detaching panels is useful when you want to maximize the editor area while still keeping certain panels accessible as tabs. - ---- - -## Working with Documents - -### Creating Documents - -![File Menu](illustrations/file-menu.svg) - -**From the menu:** - -- **File → New doc** (`Ctrl+N`) - Creates a new untitled document - -**From Project Explorer:** - -- Right-click on a folder → **New file** -- Enter a filename (include .md extension) - -### Opening Documents - -**From the menu:** - -- **File → Open file...** (`Ctrl+O`) - Opens a file dialog with extension filters: - - **Markdown** (`*.md`, `*.markdown`) - - **Text** (`*.txt`) - - **All files** (`*.*`) -- **File → Open project...** - Opens a directory chooser to select a project folder -- **File → Recent** - Shows recently opened files and projects - -**From Project Explorer:** - -- Double-click any Markdown file to open it in a new tab - -### Recent Files & Projects - -The **File → Recent** submenu is organized into two sections: - -- **Files** — recently opened files, displayed as `name (parent directory)` -- **Projects** — recently opened project directories -- **Clear History** — clears all recent entries - -If a recent file or directory no longer exists on disk, an error dialog is shown and the entry is automatically removed. - -### Saving Documents - -- **File → Save** (`Ctrl+S`) - Save the current document -- **File → Save as...** (`Ctrl+Shift+S`) - Save with a new name - -When you try to close a modified document without saving, MarkNote will prompt you to save your changes. - -### Project Session Restore - -MarkNote automatically remembers which documents you had open when you close a project and reopens them the next time you open the same project. - -- **How it works:** When you close the app or switch projects, the list of open files is saved to a `.marknote` file at the root of yourproject directory (one relative path per line, under a `[open_files]` section). -- **On next open:** Files listed in `.marknote` are restored in the same order; files that no longer exist on disk are silently skipped. -- **Transparent:** The `.marknote` file is hidden in the Project Explorer, just like `.marknote-index.json`. - -> **Note:** Session restore is independent from the **Reopen last project on startup** option, which controls whether the project folder itself is reopened, not which documents were open inside it. - ---- - -### Working with Tabs - -- Click on a tab to switch to that document -- Click the **×** button on a tab to close it -- Press `Ctrl+W` to close the active tab -- Press `Ctrl+Shift+W` to close **all** tabs at once -- **Right-click** on any tab for additional close actions: - - **Close All Tabs** — closes every open tab - - **Close All But This** — keeps only the tab you right-clicked - - **Close Tabs to the Left** — closes all tabs to the left of the current one -- **Drag tabs** to reorder them within the tab bar -- Modified documents show a **\*** prefix in the tab title -- Tab names longer than 15 characters are truncated with an ellipsis (`…`); hover for the full name - -### Drag & Drop into the Editor - -You can drag files from the Project Explorer directly into the editor to insert Markdown links: - -![Drag & Drop Links](illustrations/drag-drop-editor.svg) - -- **Image files** (`.png`, `.jpg`, `.gif`, `.svg`) → inserts `![filename](relative/path)` -- **Markdown files** (`.md`) → inserts `[front matter title](relative/path)` (uses the front matter title if available, otherwise the filename) - -The link is inserted at the exact drop position in the text. - ---- - -## Search & Replace in Editor - -MarkNote includes a **Search & Replace overlay bar** that floats over the editor without interrupting your layout. It appears just below the Front Matter panel and only takes up the space it needs. - -### Opening the Bar - -| Method | Result | -|--------|--------| -| `Ctrl+F` or **Edit → Search...** | Opens the bar with the **search field only** | -| `Ctrl+H` or **Edit → Search and Replace...** | Opens the bar with **both search and replace fields** | -| `Escape` or **✕ button** | Closes the bar and removes all highlights | - -### Search Field & Options - -The search row contains: - -| Control | Description | -|---------|-------------| -| **Search field** | Type your query here; results are highlighted as you type | -| `.*` toggle | Enable **Regular Expression** mode | -| `\b` toggle | Enable **Full Word** matching | -| `Aa` toggle | Enable **Match Case** (case-sensitive search) | -| `▲` button | Navigate to the **previous** occurrence (`Shift+Enter` also works) | -| `▼` button | Navigate to the **next** occurrence (`Enter` also works) | -| **Counter** | Shows the current position and total count (e.g., `2 / 7`) or `No results` | -| `✕` button | Close the bar | - -### Replace Field - -Visible only when opened via `Ctrl+H` or **Edit → Search and Replace...**: - -| Control | Description | -|---------|-------------| -| **Replace field** | The replacement text (plain text, no regex syntax required) | -| **Replace** button | Replace the **currently highlighted** occurrence and move to the next | -| **Replace all** button | Replace **all** occurrences at once | - -### Occurrence Highlighting - -- All occurrences are highlighted in **yellow** (`search-highlight`) -- The **currently selected** occurrence is highlighted in **orange** (`search-highlight-current`) and the editor scrolls to it -- When the bar is closed, all highlights are removed and normal syntax highlighting is restored - -### Regex Mode - -When the `.*` toggle is active: - -- The query is interpreted as a Java regular expression -- If the pattern is invalid, the search field turns **red** and a `⚠` warning is shown -- Capture groups can be used in the Replace field (e.g., `$1`) - ---- - -## Editor Context Menu & Floating Toolbar - -MarkNote provides two complementary ways to apply Markdown formatting without manually typing syntax: a **right-click context menu** and a **floating toolbar** that appears above any text selection. - -### Editor Context Menu - -Right-click anywhere in the editor to open the context menu: - -| Item | Shortcut | Condition | -|------|----------|-----------| -| **Copy** | `Ctrl+C` | Requires selection | -| **Cut** | `Ctrl+X` | Requires selection | -| **Paste** | `Ctrl+V` | Always available | -| *(separator)* | | | -| **Title H1** | `Ctrl+1` | Acts on current line | -| **Title H2** | `Ctrl+2` | Acts on current line | -| **Title H3** | `Ctrl+3` | Acts on current line | -| **Title H4** | `Ctrl+4` | Acts on current line | -| **Title H5** | `Ctrl+5` | Acts on current line | -| **Title H6** | `Ctrl+6` | Acts on current line | -| *(separator)* | | | -| **Bold** | `Ctrl+B` | Requires selection | -| **Italic** | `Ctrl+I` | Requires selection | -| *(separator)* | | | -| **Insert link** | `Ctrl+K` | Requires selection (becomes the URL) | -| **Insert image** | `Ctrl+J` | Requires selection (becomes the path) | -| **Insert code block** | `Ctrl+E` | Requires selection | - -#### Heading toggle - -Applying a heading level (`Ctrl+1`…`Ctrl+6`) acts on the **current line** (no selection needed). If the line already has the same heading level, the prefix is removed (toggle off). - -#### Bold / Italic toggle - -Bold (`Ctrl+B`) and Italic (`Ctrl+I`) **wrap** the selected text with `**` or `*`. If the selection is already wrapped, the markers are removed instead. - -#### Insert link / image - -- **Insert link** (`Ctrl+K`): wraps the selection as the URL → `[](selection)`, caret lands inside `[`. -- **Insert image** (`Ctrl+J`): same but with image syntax → `![](selection)`, caret lands inside `![`. -- **Insert code block** (`Ctrl+E`): wraps the selection in a fenced code block. - -### Floating Formatting Toolbar - -Whenever you **select text** in the editor, a compact floating toolbar appears just above the selection: - -| Button | Action | -|--------|--------| -| **B** | Bold (`**…**`) | -| **I** | Italic (`*…*`) | -| **Lien** | Insert link | -| **Img** | Insert image | -| **** | Insert code block | -| **H1** | Apply Heading 1 | -| **H2** | Apply Heading 2 | -| **H3** | Apply Heading 3 | -| **H4▾** | Dropdown for H4, H5, H6 | - -The toolbar **auto-hides** as soon as the selection is cleared, or when the editor loses focus. Clicking any button applies the formatting and hides the toolbar. - -> **Tip:** The floating toolbar and the context menu expose the same formatting actions — use whichever fits your workflow. - ---- - -## Front Matter Panel - -The Front Matter panel is a **collapsible pane** located above the editor text area. It provides a visual interface for editing YAML front matter metadata without manually writing YAML. - -![Front Matter Panel](illustrations/front-matter-panel.svg) - -### Supported Fields - -| Field | Description | -|-------|-------------| -| **Title** | Document title | -| **Tags** | Comma-separated tags for categorization | -| **Authors** | Document author(s) | -| **Summary** | Brief description of the document | -| **UUID** | Unique identifier (auto-generated if absent) | -| **Created At** | Creation date (`YYYY-MM-DD` or `YYYY-MM-DD HH:mm`, auto-set for new documents) | -| **Draft** | Checkbox indicating if the document is a draft | -| **Custom fields** | Any extra YAML keys are preserved and displayed in *italics* | - -### Auto-Generated Fields - -- **UUID**: When you open a Markdown file with front matter but no UUID, one is automatically generated and added. -- **Created At**: New documents automatically get today's date. - -### Document Linking via Drag & Drop - -You can create **UUID-based links** between documents by dragging `.md` files onto the Front Matter panel: - -![Front Matter Links](illustrations/front-matter-links.svg) - -1. Drag a `.md` file from the Project Explorer onto the Front Matter panel -2. A blue dashed border appears as visual feedback during drag-over -3. The dropped file's UUID is extracted (or auto-generated if the file doesn't have one) -4. The link appears in the collapsible **"Links"** sub-section with a badge showing the count (e.g., "Links (3)") - -### Managing Links - -- Each link shows as a **clickable hyperlink** displaying the UUID (with a tooltip showing the target document's title) -- Click a link to **open the linked document** in a new tab -- Click the **✕ button** next to a link to remove it -- Links are rendered in the **preview** as styled link badges using the `marknote-link:` protocol - -### Expansion Behavior - -- When a document has front matter, the panel is **expanded by default** -- For new documents without front matter, the panel is **collapsed** -- You can configure the default expansion in **Help → Options... → Misc. → Front matter expanded by default** - ---- - -## Project Explorer - -The Project Explorer helps you manage your project files efficiently. - -![Project Explorer](illustrations/project-explorer.svg) - -### Navigating Files - -- **Single-click** - Select a file or folder (multi-selection supported) -- **Double-click** - Open a file in the editor -- **Expand/Collapse** - Click the arrow icons to navigate folders - -> **Note:** Markdown files display their **front matter title** instead of the filename in the tree. Hover for a tooltip showing `"filename — title"`. Files and folders starting with `.` are hidden. - -### File Organization - -- **Directories appear first**, followed by files -- Both are sorted **alphabetically** (case-insensitive) -- File/folder **icons** are displayed for visual distinction - -### Context Menu - -Right-click on files or folders to access: - -| Action | Description | -|--------|-------------| -| **New file** | Create a new file in the selected folder | -| **New folder** | Create a new subfolder | -| **Rename...** | Rename the selected item | -| **Delete** | Delete the selected item (with confirmation) | -| **Reset index** | Rebuild the project search index (root folder only) | - -### Drag and Drop - -- **Move files (internal)** - Drag files/folders within the explorer to reorganize your project. The destination folder highlights in **light blue** (MOVE mode). A confirmation dialog shows the file names being moved. -- **Copy external files** - Drag files from your file manager into MarkNote. The destination folder highlights in **light green** (COPY mode). A confirmation dialog shows the file names being copied. -- **Multi-file selection** - Select multiple files/folders and drag them together. - -> **Note:** For 4 or more files, the confirmation dialog shows the count instead of individual names. - -### Supported File Types - -| Type | Extensions | Action | -|------|------------|--------| -| Markdown | `.md`, `.markdown` | Opens in editor with preview | -| Text | `.txt`, `.text` | Opens in editor | -| Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.bmp`, `.webp`, `.svg` | Opens in image preview | -| CSS | `.css` | Opens with CSS syntax highlighting | - -> **Tip:** If the project is a Git repository, each file also shows a small **colored status dot** — see [Git Support](#git-support) for details. - -### Image Preview - -When you open an image file, it is displayed in a dedicated **Image Preview tab**: - -![Image Preview](illustrations/image-preview.svg) - -- **Info banner** at the top shows: file format (uppercase) and dimensions (e.g., `PNG | 800 × 600 px`) -- **Zoom** with the scroll wheel (range: 10% – 1000%, factor 1.1× per step) -- **Zoom level overlay** appears centered (e.g., `"150%"`) and fades out after 2 seconds -- **Pan** the image by dragging when zoomed in -- Supported formats: `png`, `jpg`, `jpeg`, `gif`, `bmp`, `webp`, `svg` - ---- - -## Git Support - -MarkNote integrates with Git when a project folder contains a `.git/` subdirectory. No manual activation is required — git support is enabled automatically when you open such a project. - -### Git Status Indicators - -Each **file** (not folder) in the Project Explorer tree shows a small colored dot indicating its Git status: - -| Dot colour | Status | Meaning | -|------------|--------|---------| -| 🟢 Green | `CLEAN` | Tracked, no local changes | -| 🟡 Orange | `MODIFIED` | Tracked and modified (or deleted) in the working tree | -| 🔵 Blue | `STAGED` | Added to the index, not yet committed | -| 🔴 Red | `UNTRACKED` | Not managed by Git | - -The dots are refreshed automatically after every file operation (create, rename, delete, move, copy). They are also refreshed after each Sync operation. - -### Explorer Toolbar - -When a project is open, a toolbar appears at the top of the Project Explorer panel. It contains up to two buttons: - -- **↻ Index** — Always visible when a project is loaded. Refreshes the file tree (picking up files added outside of MarkNote) and rebuilds the search index from scratch. -- **⇅ Sync** — Only visible when the project is a Git repository (hidden otherwise). - -#### Sync - -Clicking **Sync** performs three sequential operations in a background thread: - -1. **Commit local changes** (if any modified/untracked files exist) - - Runs `git add -A` to stage all changes - - Creates an automatic commit with a structured message: - - ``` - [MarkNote sync] 2026-02-25 14:32:05 @ my-laptop - - Modified: - - docs/note1.md - - docs/chapter2.md - ``` - - - The message includes the current date/time, the machine's hostname, and the list of affected files - - This step is **skipped** if there are no local changes - -2. **Pull** — runs `git pull --rebase` to fetch and integrate remote changes without creating a merge commit - -3. **Push** — runs `git push` to send all local commits to the remote - -Once complete, a **result dialog** appears showing the combined output of all operations. If any step fails, the error output from git is displayed in the same dialog. - -### Authentication - -Configure credentials in **Help → Options… → Git tab**. - -| Method | When to use | -|--------|-------------| -| **SSH key (passphrase-less)** | SSH remote URLs (`git@github.com:...`). Point to your private key file (e.g. `~/.ssh/id_ed25519`). The key must have **no passphrase** (V1 limitation). | -| **Personal access token** | HTTPS remote URLs (`https://github.com/...`). Enter your GitHub / GitLab token and the associated username (typically `token` for GitHub, `oauth2` for GitLab). | - -> **Note:** Credentials are stored in plain text in `~/.marknote/config`. For shared machines, prefer SSH keys with file-system-level permissions rather than tokens. - ---- - -## Search & Indexing - -MarkNote automatically indexes all Markdown files in your project to enable fast searching. - -![Search Box](illustrations/search-box.svg) - -### How Indexing Works - -When you open a project, MarkNote scans all Markdown files and extracts metadata from their YAML front matter: - -```yaml ---- -title: Getting Started Guide -tags: tutorial, beginner, guide -authors: John Doe -summary: A step-by-step introduction to MarkNote -uuid: 550e8400-e29b-41d4-a716-446655440000 -draft: false ---- -``` - -The index is stored as a `.marknote-index.json` file in your project root (hidden from the Project Explorer). - -### Incremental Updates - -The index is automatically kept up to date: - -- **Creating a file** - The new file is immediately indexed -- **Saving a file** - Front matter changes are re-indexed -- **Renaming a file** - The index entry is updated -- **Moving files** - Paths are updated in the index -- **Deleting a file** - The entry is removed from the index -- **Copying files** - New entries are added for the copies - -### Using the Search Box - -The search box is located in the top-right corner of the menu bar: - -1. Click the search field or start typing -2. Results appear instantly in a dropdown popup (up to **20 results**) -3. Each result shows: - - **Document title** (bold, 13px) - - **Match context** (gray, 11px — e.g., "Tag: java", "Title: Getting Started") - - **File path** (italic gray, 10px, relative to the project root) -4. Click a result or press `Enter` (selects first result) to open the document -5. Press `Down Arrow` to navigate the results list -6. Press `Escape` to dismiss the results and clear the field - -Search matches against: - -- Document title -- Filename -- Tags -- Summary -- Authors -- UUID - -### Index Persistence - -The index is stored as a `.marknote-index.json` file at the project root. It is **loaded automatically** when you reopen a project, avoiding re-indexing. The index contains: file paths, filenames, UUIDs, titles, authors, tags, summaries, creation dates, draft status, links, and tag counts. - -### Rebuilding the Index - -If the index becomes out of sync (e.g. files added or removed outside of MarkNote), you can rebuild it in two ways: - -**Using the toolbar button (recommended):** -Click the **↻ Index** button at the top of the Project Explorer. This refreshes the file tree and regenerates the index from scratch. - -**Using the context menu:** - -1. Right-click on the **root folder** in the Project Explorer -2. Select **Rebuild index** -3. The index will be regenerated from scratch - -> **Note:** The context menu "Rebuild index" option only appears on the root project folder. - ---- - -## Tag Cloud - -The Tag Cloud panel provides a visual overview of all tags used across your project. - -![Tag Cloud](illustrations/tag-cloud.svg) - -### How It Works - -The Tag Cloud displays all tags extracted from your documents' YAML front matter (`tags:` field). Each tag is displayed as a clickable label with: - -- **Font size proportional to frequency** - Tags used in many documents appear larger (up to 28px), while rare tags appear smaller (down to 11px) -- **Color variation** - Tags are displayed in different colors for visual distinction - -### Interacting with Tags - -- **Click a tag** to immediately search for all documents tagged with it. The search term is entered in the Search Box and matching results are shown. -- **Hover** over a tag to see it highlighted - -### Tag Cloud Location - -The Tag Cloud panel appears below the Project Explorer in the left panel. It can be closed using the **×** button in its header. - -### Keeping Tags Updated - -The Tag Cloud updates automatically whenever the project index changes: - -- Opening a project -- Creating, saving, renaming, or deleting documents -- Rebuilding the index - ---- - -## Network Diagram - -The Network Diagram panel provides an interactive visualization of the relationships between your documents. - -![Network Diagram](illustrations/network-diagram.svg) - -### How It Works - -The Network Diagram uses a **force-directed layout** algorithm to arrange your documents as a graph: - -- **Document nodes** (📄 icon) represent each Markdown file in your project -- **Tag nodes** (blue circles with `#`) represent shared tags -- **Solid edges** connect documents that reference each other via `links:` in their front matter -- **Dashed edges** connect documents to the tags they share -- **Current document** is highlighted with an **orange border** and cream-colored fill -- **Isolated nodes** (no connections) are automatically hidden from the diagram -- **Document groups** — disconnected clusters of documents are circled with pastel-colored halos for easy identification - -The physics simulation runs until the layout stabilizes (~60 frames below velocity threshold), then the view **automatically zooms to fit** all nodes with a 40px margin. - -### Document Groups - -When your project contains multiple independent clusters of documents (no links or shared tags between them), each cluster is displayed inside a **pastel-colored circle**. This helps you visualize which documents form isolated groups. - -- **Click a group circle** — Zoom to fit the group in view -- **Double-click a group circle** — Name the group; a dialog prompts for a name which is then displayed as a label and persisted in the index - -### Detaching the Diagram - -You can detach the Network Diagram into its own tab for a larger view: - -1. Click the **Detach** button (↗) in the panel header -2. The diagram opens in a new tab alongside your documents -3. When you close the tab, the diagram returns to the side panel (configurable in Options) - -### Automatic Label Hiding - -To keep the diagram readable at high zoom-out levels, document labels are automatically hidden when: - -- Zoom level is below 50% **and** there are more than 20 documents -- Zoom level is below 30% **and** there are more than 10 documents - -This prevents visual clutter and improves performance with large projects. - -### Navigation & Interaction - -| Action | Description | -|--------|-------------| -| **Click a document node** | Open that document in the editor | -| **Click a tag node** | Show a search popup listing all documents with that tag | -| **Click an edge** | Open the nearest connected document node | -| **Drag a node** | Move a node to rearrange the layout (pins during drag, unpins on release) | -| **Middle-click + drag** | Pan the view | -| **Scroll wheel** | Zoom in/out (range: 0.1× – 8.0×, factor 1.15× per step, centered on cursor; works on all platforms including Linux) | -| **Ctrl + Click** | Reset zoom and recenter (zoom-to-fit) | - -### Tooltips - -- **Document node hover**: shows title (bold), author, and creation date -- **Tag node hover**: shows `#tagname` and the number of connected documents - -### Tag Search Popup - -When you click a tag node in the diagram, a popup appears showing all documents that contain that tag. Each result displays: - -- **Document title** (bold) -- **Match context** (e.g., "tag: java") -- **File path** (relative to the project root) - -Click a result to open the document directly. You can also navigate the popup with the keyboard (`Enter` to select, `Escape` to close). - -### Front Matter Links - -To create links between documents, add a `links:` field in your YAML front matter with **UUID references**: - -```yaml ---- -title: My Document -tags: java, tutorial -links: - - 550e8400-e29b-41d4-a716-446655440000 - - 7c9e6679-7425-40de-944b-e07fc1f90ae7 ---- -``` - -These links appear as solid lines connecting the two documents in the diagram. You can also create links by **dragging `.md` files onto the Front Matter panel** (see [Front Matter Panel](#front-matter-panel)). - -### Keeping the Diagram Updated - -The Network Diagram updates automatically whenever: - -- A project is opened -- Files are created, saved, renamed, moved, or deleted -- The index is rebuilt - ---- - -## Status Bar - -The status bar is displayed at the bottom of the main window and provides at-a-glance information about your current work. - -![Status Bar](illustrations/status-bar.svg) - -### Sections - -| Section | Content | -|---------|----------| -| **Document & Position** | Name of the active document and cursor position (Ln/Col) | -| **Statistics** | Number of indexed documents, lines in the current document, and word count | -| **PlantUML indicator** | Spinning ⚙ gear during local-jar rendering + "● PlantUML: local jar" badge (only visible when local mode is active in Options → Tools) | -| **Indexing Progress** | A progress bar shown while the indexing service is running | - -### Background Indexing - -When a full index build or rebuild is triggered, the indexing runs in a **background thread** so that your editing is never interrupted. The progress bar shows the indexation progress in real-time. Once complete, the status returns to "Ready". - ---- - -## Live Preview - -The Preview panel shows your Markdown rendered as HTML in real-time. - -### Navigation - -| Button | Description | -|--------|-------------| -| **◀** | Go back to previous state | -| **▶** | Go forward | -| **↻** | Refresh the preview | -| **×** | Close the preview panel | - -### Scroll Synchronization - -The editor and the preview panel scroll in sync automatically. As you scroll through the editor text, the preview adjusts so that the visible rendered output matches the text around the cursor. This makes it easy to keep your focus in the same place while checking how the Markdown renders. - -> **Tip:** If the previews gets out of sync (e.g., after a large paste), click the **↻ Refresh** button to reset the layout. - -### Clicking Links - -When you click a Markdown link in the preview: - -- **Local Markdown files** (`.md`, `.markdown`, relative paths) - Open in a new MarkNote tab -- **UUID-based links** (`marknote-link:uuid`) - Resolved by searching all project files for the matching UUID, then opened in a tab -- **External URLs** - Open in your default browser - -### Collapsible Front Matter Block - -When a document has YAML front matter, the preview renders it as a **styled, collapsible `
/` block** at the top: - -![Preview Front Matter](illustrations/preview-front-matter.svg) - -- **Title** displayed as a heading -- **Draft badge** ("✎ Draft" in red) if `draft: true` -- **UUID** in monospace -- **Author** and **Date** -- **Tags** as styled color badges -- **Summary** in italics -- **Linked documents** as clickable link badges (resolved from UUID to title) - -### Supported Markdown Features - -MarkNote supports standard Markdown syntax plus extensions: - -```markdown -# Headings (H1 through H6) - -**Bold text** and *italic text* - -`Inline code` and code blocks - -- Bullet lists -- With multiple items - -1. Numbered lists -2. Work too - -[Links](https://example.com) - -![Local images](path/to/image.png) -![External images](https://example.com/image.png) - -> Blockquotes - -> [!NOTE] -> GitHub-style alerts - ---- -Horizontal rules ---- - -- [ ] Unchecked task -- [x] Completed task - -| Tables | Are | Supported | -|--------|-----|-----------| -| Data | Goes| Here | -``` - -### Images - -MarkNote supports both local and external images in the preview: - -- **Local images:** Relative paths from your project directory -- **External images:** HTTP/HTTPS URLs from the internet - -```markdown -![Local screenshot](./images/screenshot.png) -![Web image](https://example.com/image.png) -``` - -> **Note:** External images require an internet connection. If the image URL is unreachable, a broken image placeholder will be displayed. - -### Code Syntax Highlighting - -Fenced code blocks are automatically highlighted with language detection: - -````markdown -```java -public class Hello { - public static void main(String[] args) { - System.out.println("Hello, world!"); - } -} -``` -```` - -The syntax highlighting theme automatically adapts to your chosen application theme: - -| App Theme | highlight.js Style | Code Background | Code Foreground | -|---|---|---|---| -| Light | `github` | `#f6f8fa` | `#24292e` | -| Dark | `github-dark` | `#282c34` | `#e6e6e6` | -| Solarized Light | `stackoverflow-light` | `#fdf6e3` | `#657b83` | -| Solarized Dark | `stackoverflow-dark` | `#002b36` | `#93a1a1` | -| High Contrast | `a11y-dark` | `#1a1a1a` | `#f8f8f2` | - -Custom themes are detected via a **"Based on:" comment** in the CSS header, with a heuristic fallback (dark background → dark syntax theme). - -### Code Block Copy Button - -Every code block in the preview displays a **"Copy" button** on hover (top-right corner). Clicking it copies the code to the clipboard, and the button briefly shows **"✓ Copied"** with a green background for 1.5 seconds. - -### Task Lists (Checkboxes) - -MarkNote supports GitHub-style task lists (checkboxes) in the preview: - -```markdown -- [ ] This is an unchecked task -- [x] This is a completed task -- [X] Uppercase X also works -``` - -In the preview, these render as: - -- ☐ This is an unchecked task -- ☑ This is a completed task -- ☑ Uppercase X also works - -> **Note:** Checkboxes in the preview are read-only (disabled). To change the state, edit the Markdown source directly. - -### GitHub Alerts - -MarkNote supports GitHub-style alerts (also known as admonitions) for highlighting important information in blockquotes: - -```markdown -> [!NOTE] -> Useful information that users should know. - -> [!TIP] -> Helpful advice for doing things better or more easily. - -> [!IMPORTANT] -> Key information users need to know. - -> [!WARNING] -> Urgent info that needs immediate user attention. - -> [!CAUTION] -> Advises about risks or negative outcomes. -``` - -These render as styled boxes with colored borders and icons: - -| Alert Type | Color | Use Case | -|------------|-------|----------| -| **Note** | Blue | General information | -| **Tip** | Green | Helpful hints and best practices | -| **Important** | Purple | Critical information | -| **Warning** | Yellow | Potential issues or caveats | -| **Caution** | Red | Dangerous actions or irreversible operations | - -> **Tip:** GitHub Alerts work great for documentation, tutorials, and user guides where you need to draw attention to specific information. - -### PlantUML Diagrams - -Embed PlantUML diagrams directly in your Markdown using fenced code blocks: - -````markdown -```plantuml -@startuml -Alice -> Bob: Hello -Bob --> Alice: Hi! -@enduml -``` -```` - -> **Note:** If a PlantUML code block doesn't start with `@start`, it is **automatically wrapped** with `@startuml` / `@enduml`. - -#### Rendering Mode - -By default, diagrams are rendered by the **official PlantUML online server** (`https://www.plantuml.com/plantuml/svg/`). If an internet connection is unavailable or you prefer privacy, you can configure a **local `plantuml.jar`** instead. - -| Mode | How it works | Setup needed | -|------|-------------|---------------| -| **Online server** (default) | Encodes the diagram source and fetches an SVG from plantuml.com | None | -| **Local jar** | Runs `java -jar plantuml.jar -pipe -tsvg` in a background process per diagram block; injects the SVG directly into the page when ready | Configure in **Options → Tools** | - -When local rendering is active: - -- Each diagram is replaced temporarily by a *"⏳ Rendering diagram…"* placeholder -- Background threads render each block independently -- The **⚙ spinning gear** icon in the status bar is visible during rendering -- On completion the placeholders are replaced inline with the SVG (no page reload) -- If the local jar fails, the diagram falls back silently to the online server -- **SVG Cache:** Generated SVG images are cached in memory using a SHA-256 hash of the diagram source; unchanged diagrams are served instantly from cache without invoking the jar, significantly improving preview responsiveness during editing - -See [Options → Tools Tab](#tools-tab) to configure the local jar. - -### Mermaid Diagrams - -Mermaid diagrams are also supported: - -````markdown -```mermaid -graph LR - A[Start] --> B{Decision} - B -->|Yes| C[OK] - B -->|No| D[Cancel] -``` -```` - -Mermaid diagrams are rendered client-side in the preview panel. The Mermaid theme **automatically matches** your application theme: dark themes use the Mermaid `"dark"` theme, while light themes use `"default"`. - -### Math Equations (KaTeX) - -MarkNote supports LaTeX math notation via KaTeX: - -- **Inline math:** `$E = mc^2$` renders as an inline equation -- **Block math:** - -```markdown -$$ -\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi} -$$ -``` - -### Image Sizing - -MarkNote extends the standard Markdown image syntax to allow specifying image dimensions using `=WIDTHxHEIGHT` at the end of the image declaration: - -```markdown -![alt text](path/to/image.png "optional title" =100x20) -``` - -This renders the image with `width="100"` and `height="20"` attributes. - -You can also specify only width or only height: - -| Syntax | Result | -|--------|--------| -| `![photo](pic.png =300x200)` | Width 300px, Height 200px | -| `![photo](pic.png "title" =400x)` | Width 400px, height auto | -| `![photo](pic.png =x150)` | Width auto, height 150px | - -> **Note:** Without the `=WxH` suffix, images behave as standard Markdown images and scale automatically to fit the preview. - ---- - -## Reading Mode - -Reading Mode provides a distraction-free, fullscreen environment for reading and reviewing your documents without any editing UI in the way. - -### Entering Reading Mode - -| Method | Description | -|--------|-------------| -| **View → Enter Reading Mode** | Menu item in the View menu | -| `Ctrl+Shift+P` | Keyboard shortcut | - -When reading mode is activated: - -- The application goes **fullscreen** -- All side panels (Project Explorer, Tag Cloud, Network Diagram, LLM Chat) are **hidden** -- The **editor tab bar** is removed from the layout -- The **Preview panel** expands to fill the entire screen -- The **menu bar** is replaced by a thin bar containing only an **Exit Reading Mode** button -- The **Project Explorer** reappears as a compact **floating overlay** panel pinned to the top-left corner of the screen - -### The Floating Project Explorer - -In reading mode, the Project Explorer stays accessible as a floating panel so you can navigate between files without leaving the reader: - -| Control | Description | -|---------|-------------| -| **▾ minimize button** | Collapses the panel to just its title bar, clearing the reading area | -| **▴ maximize button** | Restores the full panel after minimizing | - -The **close (×)** and **detach (⇱)** buttons are hidden in reading mode — the panel is always visible and cannot be closed or detached while reading. - -### Exiting Reading Mode - -| Method | Description | -|--------|-------------| -| **Exit Reading Mode** button | Click the button in the top-right bar | -| `Escape` / exit fullscreen | Exiting fullscreen (F11 on Linux/Windows, `Cmd+Ctrl+F` on macOS) also exits reading mode | - -When reading mode exits: - -- All panels that were visible before are **restored** to their original docked positions -- The **split divider positions** (panel widths, editor/preview ratio) are fully restored to what they were before entering reading mode -- The editor tab bar and the menu bar return - ---- - -## LLM Chat - -MarkNote can connect to a local or remote Large Language Model and provide an integrated chat workflow alongside your notes. - -![LLM Chat Panel](illustrations/llm-chat-panel.svg) - -### What the Panel Does - -The **LLM Chat** panel is designed for note drafting, reformulation, summarization, and content generation without leaving the editor. - -It supports: - -- **Streaming responses** while the model is generating text -- **Conversation history** for the current session -- **System context** to steer the assistant's behavior -- **Message actions** to copy, export, edit, or insert content into the active document -- **Session export** and **session insertion** into the active document -- **Cancellation** of the current request -- **Welcome message** — the panel displays a configuration summary as the first message each time a new session starts (endpoint, model, timeout, API type, system context status) -- **Markdown rendering** — assistant responses are displayed as rendered Markdown (headings, code blocks, bold/italic, tables…), making structured answers easy to read -- **Document context selection** — a bar above the prompt field lists all open documents as toggle buttons; selected documents are included as context when you send a prompt - -### Supported Backends - -MarkNote currently supports: - -- **Ollama** endpoints such as `http://localhost:11434` -- **OpenAI-compatible chat endpoints** using the `/v1/chat/completions` format - -The application automatically adapts the request format based on the configured endpoint URL. - -### Enabling the Feature - -1. Open **Help → Options...** -2. Go to the **LLM** tab -3. Check **Enable LLM panel** -4. Configure your endpoint, model, and optional API key -5. Click **OK** - -If the feature is enabled, the panel becomes available in the interface and in **View → LLM Chat**. - -### Configuring the Connection - -The LLM tab lets you define: - -| Option | Description | -|--------|-------------| -| **Enable LLM panel** | Shows or hides the LLM Chat panel in the main UI | -| **API Endpoint URL** | Base URL of your LLM service | -| **API Key** | Optional bearer token; usually not required for local Ollama | -| **Model** | Model identifier sent with each chat request | -| **Refresh Models** | Queries the server for available models (Ollama-compatible endpoint) | -| **Timeout** | Maximum request duration in seconds | -| **Default System Context** | Default instructions automatically prepended to each conversation | - -Use **Test Connection** to validate the current settings before saving them. - -### Sending a Prompt - -1. Open the panel from **View → LLM Chat** if it is not already visible -2. Type your request in the prompt area -3. Press the **Send** button or `Ctrl+Enter` -4. Read the streamed answer as it appears in the conversation view - -While the request is running: - -- A spinner is displayed in the input area -- The prompt field is temporarily disabled -- The **Cancel** button replaces the Send button - -### Document Context Selection - -Above the prompt text area, a **document context bar** shows a compact toggle button for each open document tab. You can include one or more open documents as additional context for the model. - -**How to use it:** - -1. The bar appears automatically when at least one document tab is open -2. Click a document button to **toggle it on** (selected) — the button appears highlighted -3. Selected documents' full text is appended to your prompt before it is sent to the model -4. Click again to **deselect** a document and exclude it from the context -5. The selection is preserved across prompts in the same session and updated when tabs are opened or closed - -> **Tip:** Use this feature to ask the LLM to summarize, compare, or cross-reference multiple notes without having to copy and paste their content manually. - -### System Context - -The **System Context** button in the input area opens a dialog where you can define instructions for the assistant, for example: - -- Tone and style rules -- Output format constraints -- Writing goals for the current project - -This context is saved in your LLM configuration and is automatically included in future requests. - -### Working with Messages - -Each conversation entry offers quick actions: - -| Action | Description | -|--------|-------------| -| **Copy** | Copy the message content to the clipboard | -| **Export** | Save the message as a Markdown file | -| **Insert into document** | Insert the message content into the active document | -| **Edit** | Available on user prompts; reloads the prompt into the input area and removes later messages so you can regenerate from that point | - -### Session Actions - -The panel header also provides actions for the full conversation: - -| Action | Description | -|--------|-------------| -| **Export Session** | Save the full conversation as a Markdown file | -| **Insert session into document** | Insert the entire conversation into the active document | -| **Clear Session** | Remove all messages from the current chat | - -When a full session is inserted into a document, user prompts are prefixed with `>` so the exchange remains readable in Markdown. - -### Typical Workflow - -1. Open a Markdown note -2. Ask the assistant to summarize, rewrite, or expand your content -3. Review the streamed answer in the LLM Chat panel -4. Insert the whole answer or selected messages into the document -5. Continue editing directly in MarkNote - ---- - -## Splash Screen & About - -### Splash Screen - -When MarkNote starts, a themed splash screen is displayed showing: - -- The **application logo** (centered at the top) -- The application name and version -- Author and contact information -- Copyright notice - -Click anywhere on the splash screen to dismiss it and continue to the main window. - -The splash screen follows the current application theme (Light, Dark, Solarized, etc.). You can disable it in **Help → Options... → Misc. → Show splash screen on startup**. - -### About Dialog - -Access the same information at any time via **Help → About**. The About dialog displays the same content as the splash screen in a modal window with a **Close** button. - ---- - -## Themes - -MarkNote comes with several built-in themes and allows you to create custom themes. - -![Themes](illustrations/themes.svg) - -### Built-in Themes - -| Theme | Description | -|-------|-------------| -| **Light** | Clean white background (default) | -| **Dark** | Dark background, easy on the eyes | -| **Solarized Light** | Warm, low-contrast light theme | -| **Solarized Dark** | Popular dark theme with warm colors | -| **High Contrast** | Maximum contrast for accessibility | - -### Changing Themes - -1. Go to **Help → Options...** -2. Select the **Themes** tab -3. Click on your desired theme -4. Click **OK** to apply - -### Creating Custom Themes - -1. In the Themes options tab, click **Create theme...** -2. Enter a name for your theme -3. A copy of the currently selected theme is created (name sanitized to lowercase alphanumeric + hyphens) -4. The CSS file includes a header comment: `/* Custom Theme: name \n * Based on: basedOn */` -5. The **CSS theme editor** opens automatically (closes the options dialog) -6. Modify the CSS to customize colors and styles -7. Save the file (`Ctrl+S`) — if editing the current theme, the app theme **refreshes automatically** - -Custom themes are stored in `~/.marknote/themes/`. - -### Deleting Custom Themes - -- In the Themes tab, select a custom theme and click **Delete Theme** -- Built-in themes cannot be deleted -- A confirmation dialog is shown before deletion - -### Theme List Formatting - -In the Themes options tab: - -- **Italic** names = built-in themes -- **Bold** names = custom themes -- **Double-click** a custom theme to open it in the CSS editor - -### CSS Theme Editor - -The CSS theme editor provides a full editing experience with **syntax highlighting** for: - -- Comments, strings, hex colors -- Numbers (with CSS units) -- Pseudo-classes, selectors, properties -- Braces and punctuation - -Modification indicator (**\*** prefix) and save/close confirmation work the same as document tabs. - -### Theme CSS Structure - -```css -/* Main editor colors */ -.code-area { - -fx-background-color: #1e1e1e; - -fx-text-fill: #abb2bf; -} - -/* Markdown syntax highlighting */ -.heading { -fx-fill: #c678dd; } -.bold { -fx-fill: #e06c75; } -.italic { -fx-fill: #98c379; } -.code { -fx-fill: #61afef; } -``` - ---- - -## Options & Settings - -Access settings via **Help → Options...** or by pressing the shortcut shown in the menu. - -### Misc. Tab - -| Option | Description | -|--------|-------------| -| **Number of recent files/projects** | How many items to show in Recent menus (1-50) | -| **Create document on startup** | Automatically create a new document when starting | -| **Reopen last project on startup** | Remember and reopen your last project (shows a confirmation dialog with the project name) | -| **Restore open documents on startup** | When reopening a project, reopen the documents that were open in the previous session (session stored in `.marknote` at the project root) | -| **Show Welcome page on startup** | Display the Welcome tab when starting | -| **Show splash screen on startup** | Display the splash screen when starting (enabled by default) | -| **Front matter expanded by default** | Whether the Front Matter panel is expanded when opening documents (default: true) | -| **Reattach diagram panel when tab closes** | When enabled, the Network Diagram returns to the side panel after closing its detached tab (default: true) | -| **Language** | Choose your preferred interface language (`system` follows OS locale) | - -> **Note:** Changing the language **saves the configuration immediately** and **restarts the application**. - -### Themes Tab - -- View and select from available themes (italic = built-in, bold = custom) -- Create new custom themes based on existing ones -- Delete custom themes (built-in themes cannot be deleted) -- Double-click a custom theme to open it in the CSS editor - -### Tools Tab - -The **Tools** tab lets you configure external tools used by MarkNote. - -#### PlantUML Local Jar - -| Option | Description | -|--------|-------------| -| **Use local PlantUML jar** | Checkbox — when checked, MarkNote uses your local `plantuml.jar` instead of the online server for rendering diagrams in the preview | -| **PlantUML jar path** | Full path to your `plantuml.jar` file. Use the **Browse…** button to open a file selector filtered to `*.jar` | - -**Steps to configure:** - -1. Download `plantuml.jar` from [https://plantuml.com/download](https://plantuml.com/download) -2. Open **Help → Options…** -3. Select the **Tools** tab -4. Click **Browse…** and select your `plantuml.jar` -5. Check **Use local PlantUML jar** -6. Click **OK** - -Once enabled: - -- The status bar shows **● PlantUML: local jar** on the right side -- A **⚙ spinning gear** appears next to it while diagrams are being rendered -- The preview refreshes automatically to apply the new setting - -> **Note:** Java must be on your system `PATH` since the jar is executed as `java -jar plantuml.jar`. - -### LLM Tab - -The **LLM** tab configures the integrated **LLM Chat** panel. - -| Option | Description | -|--------|-------------| -| **Enable LLM panel** | Enables the feature and adds the panel to the main layout and View menu | -| **API Endpoint URL** | Base URL for your Ollama or OpenAI-compatible service | -| **API Key** | Optional bearer token used for authenticated services | -| **Model** | Model name sent with each request | -| **Refresh Models** | Fetches available models from an Ollama-compatible server | -| **Timeout (seconds)** | Request timeout used by the HTTP client | -| **Default System Context** | Global assistant instructions automatically prepended to each request | -| **Test Connection** | Sends a short test request to verify that the endpoint is reachable | - -> **Note:** If you disable the panel here, **View → LLM Chat** is no longer available until the feature is re-enabled. - -### Git Tab - -The **Git** tab configures credentials used when the **Sync** operation communicates with a remote repository (push/pull). - -#### SSH Authentication - -| Option | Description | -|--------|-------------| -| **SSH key path** | Full path to your private SSH key (e.g. `~/.ssh/id_ed25519` or `~/.ssh/id_rsa`). Use **Browse…** to select the file. The key must have **no passphrase** (V1 limitation). | - -#### HTTPS / Token Authentication - -| Option | Description | -|--------|-------------| -| **Username** | The username passed to git. Typically `token` for GitHub or `oauth2` for GitLab | -| **Personal access token** | Your personal access token from GitHub / GitLab settings. Stored in `~/.marknote/config` | - -> **Note:** Only one method is used per Sync. SSH is used when an SSH key path is configured; HTTPS token credentials are used otherwise. Both fields may be left empty if the remote requires no authentication (e.g., public repositories via SSH with your system key). - -### Language Settings - -MarkNote supports the following languages: - -- 🇫🇷 Français (French) -- 🇬🇧 English -- 🇩🇪 Deutsch (German) -- 🇪🇸 Español (Spanish) -- 🇮🇹 Italiano (Italian) - -To change the language: - -1. Go to **Help → Options...** -2. In the **Misc.** tab, select your language -3. The application will restart to apply the change - ---- - -## Keyboard Shortcuts - -![Keyboard Shortcuts](illustrations/keyboard-shortcuts.svg) - -### File Operations - -| Shortcut | Action | -|----------|--------| -| `Ctrl+N` | New document | -| `Ctrl+O` | Open file | -| `Ctrl+S` | Save | -| `Ctrl+Shift+S` | Save as | -| `Ctrl+W` | Close current tab | -| `Ctrl+Shift+W` | Close all tabs | -| `Ctrl+Q` | Quit application | - -### Editing - -| Shortcut | Action | -|----------|--------| -| `Ctrl+Z` | Undo | -| `Ctrl+Y` | Redo | -| `Ctrl+X` | Cut | -| `Ctrl+C` | Copy | -| `Ctrl+V` | Paste | -| `Ctrl+A` | Select all | -| `Ctrl+F` | Open Search bar (search only) | -| `Ctrl+H` | Open Search & Replace bar | -| `Ctrl+Enter` | Send the current prompt from the LLM Chat input | - -### Markdown Formatting (Editor) - -| Shortcut | Action | Requires selection | -|----------|--------|-------------------| -| `Ctrl+B` | Bold (`**…**`) — toggle | Yes | -| `Ctrl+I` | Italic (`*…*`) — toggle | Yes | -| `Ctrl+K` | Insert link `[](selection)` | Yes | -| `Ctrl+J` | Insert image `![](selection)` | Yes | -| `Ctrl+E` | Insert fenced code block | Yes | -| `Ctrl+1` | Apply / toggle Heading H1 | No (current line) | -| `Ctrl+2` | Apply / toggle Heading H2 | No (current line) | -| `Ctrl+3` | Apply / toggle Heading H3 | No (current line) | -| `Ctrl+4` | Apply / toggle Heading H4 | No (current line) | -| `Ctrl+5` | Apply / toggle Heading H5 | No (current line) | -| `Ctrl+6` | Apply / toggle Heading H6 | No (current line) | - -### Navigation - -| Shortcut | Action | -|----------|--------| -| `Ctrl+Tab` | Next tab | -| `Ctrl+Shift+Tab` | Previous tab | -| `F5` | Refresh preview | - -### View - -| Shortcut | Action | -|----------|--------| -| `Ctrl+E` | Toggle Project Explorer | -| `Ctrl+P` | Toggle Preview panel | -| `Ctrl+T` | Toggle Tag Cloud | -| `Ctrl+L` | Toggle Network Diagram | -| `Ctrl+M` | Toggle LLM Chat | -| `Ctrl+Shift+P` | Enter Reading Mode | - -> **Note:** On macOS, use `Cmd` instead of `Ctrl`. - ---- - -## Troubleshooting - -### Common Issues - -#### The preview is not updating - -1. Click the **Refresh** button (↻) in the preview panel -2. Check that the Preview panel is visible (View → Preview panel) -3. Make sure you're editing a Markdown file (.md) - -#### Files are not showing in Project Explorer - -1. Make sure you've opened a project (File → Open project...) -2. Check that the Project Explorer is visible (View → Project explorer) -3. Try refreshing by closing and reopening the project - -#### Theme changes are not applied - -1. Make sure to save your custom theme CSS file -2. If editing the current theme, close and reopen options -3. Restart MarkNote if changes still don't appear - -#### Application language didn't change - -1. The application needs to restart after changing language -2. Try closing and reopening MarkNote manually -3. Check the language setting in Options → Misc. - -#### LLM Chat does not respond - -1. Open **Help → Options... → LLM** and verify the endpoint URL and model name -2. Use **Test Connection** to confirm the service is reachable -3. Check whether your backend requires an API key -4. If you are using Ollama locally, make sure the Ollama service is running -5. Increase the timeout if your model is slow to start or answer - -### Getting Help - -If you encounter issues not covered here: - -1. Check the [GitHub repository](https://github.com/mcgivrer/marknote) for known issues -2. Submit a bug report with details about your system and the problem -3. Contact the author at - ---- - -## About MarkNote - -**Version:** 0.1.5 -**Author:** Frédéric Delorme -**Copyright:** © SnapGames 2026 -**License:** MIT -**Repository:** - ---- - -*This documentation is part of the MarkNote project. Last updated: May 2026.* diff --git a/src/main/java/utils/GitHubConnector.java b/src/main/java/utils/GitHubConnector.java new file mode 100644 index 0000000..e399d81 --- /dev/null +++ b/src/main/java/utils/GitHubConnector.java @@ -0,0 +1,194 @@ +package utils; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * Connecteur pour l'API GitHub (github.com). + * + *

Ce connecteur utilise l'API REST de GitHub v3 pour lister et créer des dépôts. + * Il nécessite un Personal Access Token (PAT) avec les permissions {@code repo}.

+ * + *

Endpoints utilisés :

+ *
    + *
  • GET /user/repos?type=owner&per_page=100 - Liste les dépôts de l'utilisateur
  • + *
  • POST /user/repos - Crée un nouveau dépôt
  • + *
+ * + * @see GitHub REST API + */ +public class GitHubConnector implements RemoteConnector { + + private static final String API_BASE_URL = "https://api.github.com"; + private static final LogService log = LogService.getInstance(); + private static final String LOG_SOURCE = "GitHubConnector"; + + private final String token; + private final HttpClient httpClient; + private final JSONParser jsonParser; + + /** + * Construit un connecteur GitHub avec le token fourni. + * + * @param token Le Personal Access Token GitHub avec permissions {@code repo} + */ + public GitHubConnector(String token) { + this.token = token; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.jsonParser = new JSONParser(); + } + + @Override + public String platform() { + return "github"; + } + + @Override + public List listRepositories() throws RemoteConnectorException { + log.debug(LOG_SOURCE, "Récupération de la liste des dépôts GitHub"); + + String url = API_BASE_URL + "/user/repos?type=owner&per_page=100&sort=updated"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/vnd.github+json") + .header("Authorization", "token " + token) + .header("X-GitHub-Api-Version", "2022-11-28") + .GET() + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + handleErrorResponse(response); + } + + return parseRepositories(response.body()); + + } catch (IOException e) { + log.error(LOG_SOURCE, "Erreur réseau lors de la récupération des dépôts: " + e.getMessage()); + throw new RemoteConnectorException("Erreur réseau: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(LOG_SOURCE, "Requête interrompue: " + e.getMessage()); + throw new RemoteConnectorException("Requête interrompue", e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void createRepository(String name, boolean isPrivate) throws RemoteConnectorException { + log.debug(LOG_SOURCE, "Création du dépôt GitHub: " + name + " (privé=" + isPrivate + ")"); + + String url = API_BASE_URL + "/user/repos"; + + // Construction du body JSON + JSONObject body = new JSONObject(); + body.put("name", name); + body.put("private", isPrivate); + body.put("auto_init", true); // Créer avec README initial + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Accept", "application/vnd.github+json") + .header("Authorization", "token " + token) + .header("X-GitHub-Api-Version", "2022-11-28") + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString())) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + handleErrorResponse(response); + } + + log.info(LOG_SOURCE, "Dépôt créé avec succès: " + name); + + } catch (IOException e) { + log.error(LOG_SOURCE, "Erreur réseau lors de la création du dépôt: " + e.getMessage()); + throw new RemoteConnectorException("Erreur réseau: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(LOG_SOURCE, "Requête interrompue: " + e.getMessage()); + throw new RemoteConnectorException("Requête interrompue", e); + } + } + + /** + * Parse la réponse JSON pour extraire la liste des dépôts. + */ + private List parseRepositories(String jsonResponse) throws RemoteConnectorException { + List repos = new ArrayList<>(); + + try { + JSONArray jsonArray = (JSONArray) jsonParser.parse(jsonResponse); + + for (Object obj : jsonArray) { + JSONObject repo = (JSONObject) obj; + + String name = (String) repo.get("name"); + String cloneUrl = (String) repo.get("clone_url"); + String description = (String) repo.get("description"); + Boolean isPrivate = (Boolean) repo.get("private"); + String defaultBranch = (String) repo.get("default_branch"); + + repos.add(new RemoteRepo( + name, + cloneUrl, + description != null ? description : "", + isPrivate != null && isPrivate, + defaultBranch != null ? defaultBranch : "main" + )); + } + + log.debug(LOG_SOURCE, "Parsed " + repos.size() + " repositories"); + return repos; + + } catch (ParseException e) { + log.error(LOG_SOURCE, "Erreur de parsing JSON: " + e.getMessage()); + throw new RemoteConnectorException("Réponse JSON invalide", e); + } + } + + /** + * Gère les réponses d'erreur HTTP et lève une exception appropriée. + */ + private void handleErrorResponse(HttpResponse response) throws RemoteConnectorException { + int statusCode = response.statusCode(); + String body = response.body(); + + String errorMessage = extractErrorMessage(body); + + log.error(LOG_SOURCE, "Erreur API GitHub " + statusCode + ": " + errorMessage); + throw new RemoteConnectorException(statusCode, errorMessage); + } + + /** + * Extrait le message d'erreur du JSON de réponse GitHub. + */ + private String extractErrorMessage(String jsonResponse) { + try { + JSONObject json = (JSONObject) jsonParser.parse(jsonResponse); + String message = (String) json.get("message"); + return message != null ? message : "Erreur inconnue"; + } catch (Exception e) { + return "Erreur inconnue"; + } + } +} diff --git a/src/main/java/utils/GitLabConnector.java b/src/main/java/utils/GitLabConnector.java new file mode 100644 index 0000000..4d0d74b --- /dev/null +++ b/src/main/java/utils/GitLabConnector.java @@ -0,0 +1,215 @@ +package utils; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * Connecteur pour l'API GitLab (gitlab.com ou instance self-hosted). + * + *

Ce connecteur utilise l'API REST de GitLab v4 pour lister et créer des dépôts (projects). + * Il nécessite un Personal Access Token avec les permissions {@code api} ou {@code write_repository}.

+ * + *

Endpoints utilisés :

+ *
    + *
  • GET /api/v4/projects?owned=true&per_page=100 - Liste les projets de l'utilisateur
  • + *
  • POST /api/v4/projects - Crée un nouveau projet
  • + *
+ * + * @see GitLab API Documentation + */ +public class GitLabConnector implements RemoteConnector { + + private static final String DEFAULT_GITLAB_URL = "https://gitlab.com"; + private static final LogService log = LogService.getInstance(); + private static final String LOG_SOURCE = "GitLabConnector"; + + private final String instanceUrl; + private final String token; + private final HttpClient httpClient; + private final JSONParser jsonParser; + + /** + * Construit un connecteur pour GitLab.com (instance publique). + * + * @param token Le Personal Access Token GitLab avec permissions {@code api} + */ + public GitLabConnector(String token) { + this(DEFAULT_GITLAB_URL, token); + } + + /** + * Construit un connecteur pour une instance GitLab custom. + * + * @param instanceUrl L'URL de base de l'instance GitLab (ex: https://gitlab.example.com) + * @param token Le Personal Access Token GitLab avec permissions {@code api} + */ + public GitLabConnector(String instanceUrl, String token) { + this.instanceUrl = instanceUrl.endsWith("/") ? instanceUrl.substring(0, instanceUrl.length() - 1) : instanceUrl; + this.token = token; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.jsonParser = new JSONParser(); + } + + @Override + public String platform() { + return "gitlab"; + } + + @Override + public List listRepositories() throws RemoteConnectorException { + log.debug(LOG_SOURCE, "Récupération de la liste des projets GitLab depuis " + instanceUrl); + + String url = instanceUrl + "/api/v4/projects?owned=true&per_page=100&order_by=updated_at&sort=desc"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("PRIVATE-TOKEN", token) + .GET() + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + handleErrorResponse(response); + } + + return parseRepositories(response.body()); + + } catch (IOException e) { + log.error(LOG_SOURCE, "Erreur réseau lors de la récupération des projets: " + e.getMessage()); + throw new RemoteConnectorException("Erreur réseau: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(LOG_SOURCE, "Requête interrompue: " + e.getMessage()); + throw new RemoteConnectorException("Requête interrompue", e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void createRepository(String name, boolean isPrivate) throws RemoteConnectorException { + log.debug(LOG_SOURCE, "Création du projet GitLab: " + name + " (privé=" + isPrivate + ")"); + + String url = instanceUrl + "/api/v4/projects"; + + // Construction du body JSON + JSONObject body = new JSONObject(); + body.put("name", name); + body.put("visibility", isPrivate ? "private" : "public"); + body.put("initialize_with_readme", true); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("PRIVATE-TOKEN", token) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString())) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + handleErrorResponse(response); + } + + log.info(LOG_SOURCE, "Projet créé avec succès: " + name); + + } catch (IOException e) { + log.error(LOG_SOURCE, "Erreur réseau lors de la création du projet: " + e.getMessage()); + throw new RemoteConnectorException("Erreur réseau: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(LOG_SOURCE, "Requête interrompue: " + e.getMessage()); + throw new RemoteConnectorException("Requête interrompue", e); + } + } + + /** + * Parse la réponse JSON pour extraire la liste des projets. + */ + private List parseRepositories(String jsonResponse) throws RemoteConnectorException { + List repos = new ArrayList<>(); + + try { + JSONArray jsonArray = (JSONArray) jsonParser.parse(jsonResponse); + + for (Object obj : jsonArray) { + JSONObject project = (JSONObject) obj; + + String name = (String) project.get("name"); + String cloneUrl = (String) project.get("http_url_to_repo"); + String description = (String) project.get("description"); + String visibility = (String) project.get("visibility"); + String defaultBranch = (String) project.get("default_branch"); + + boolean isPrivate = "private".equals(visibility); + + repos.add(new RemoteRepo( + name, + cloneUrl, + description != null ? description : "", + isPrivate, + defaultBranch != null ? defaultBranch : "main" + )); + } + + log.debug(LOG_SOURCE, "Parsed " + repos.size() + " repositories"); + return repos; + + } catch (ParseException e) { + log.error(LOG_SOURCE, "Erreur de parsing JSON: " + e.getMessage()); + throw new RemoteConnectorException("Réponse JSON invalide", e); + } + } + + /** + * Gère les réponses d'erreur HTTP et lève une exception appropriée. + */ + private void handleErrorResponse(HttpResponse response) throws RemoteConnectorException { + int statusCode = response.statusCode(); + String body = response.body(); + + String errorMessage = extractErrorMessage(body); + + log.error(LOG_SOURCE, "Erreur API GitLab " + statusCode + ": " + errorMessage); + throw new RemoteConnectorException(statusCode, errorMessage); + } + + /** + * Extrait le message d'erreur du JSON de réponse GitLab. + */ + private String extractErrorMessage(String jsonResponse) { + try { + JSONObject json = (JSONObject) jsonParser.parse(jsonResponse); + + // GitLab peut retourner "message" ou "error" + String message = (String) json.get("message"); + if (message != null) { + return message; + } + + String error = (String) json.get("error"); + if (error != null) { + return error; + } + + return "Erreur inconnue"; + } catch (Exception e) { + return "Erreur inconnue"; + } + } +} diff --git a/src/main/java/utils/GiteaConnector.java b/src/main/java/utils/GiteaConnector.java new file mode 100644 index 0000000..92f39bc --- /dev/null +++ b/src/main/java/utils/GiteaConnector.java @@ -0,0 +1,196 @@ +package utils; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * Connecteur pour l'API Gitea (instance self-hosted uniquement). + * + *

Ce connecteur utilise l'API REST de Gitea v1 pour lister et créer des dépôts. + * Il nécessite un Access Token avec les permissions appropriées.

+ * + *

Note : Gitea n'a pas d'instance publique par défaut, toutes les instances + * sont self-hosted. L'URL de base doit être fournie lors de la construction.

+ * + *

Endpoints utilisés :

+ *
    + *
  • GET /api/v1/user/repos - Liste les dépôts de l'utilisateur
  • + *
  • POST /api/v1/user/repos - Crée un nouveau dépôt
  • + *
+ * + * @see Gitea API Documentation + */ +public class GiteaConnector implements RemoteConnector { + + private static final LogService log = LogService.getInstance(); + private static final String LOG_SOURCE = "GiteaConnector"; + + private final String instanceUrl; + private final String token; + private final HttpClient httpClient; + private final JSONParser jsonParser; + + /** + * Construit un connecteur pour une instance Gitea. + * + * @param instanceUrl L'URL de base de l'instance Gitea (ex: https://gitea.example.com) + * @param token L'Access Token Gitea + */ + public GiteaConnector(String instanceUrl, String token) { + this.instanceUrl = instanceUrl.endsWith("/") ? instanceUrl.substring(0, instanceUrl.length() - 1) : instanceUrl; + this.token = token; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.jsonParser = new JSONParser(); + } + + @Override + public String platform() { + return "gitea"; + } + + @Override + public List listRepositories() throws RemoteConnectorException { + log.debug(LOG_SOURCE, "Récupération de la liste des dépôts Gitea depuis " + instanceUrl); + + String url = instanceUrl + "/api/v1/user/repos?limit=100"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "token " + token) + .GET() + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + handleErrorResponse(response); + } + + return parseRepositories(response.body()); + + } catch (IOException e) { + log.error(LOG_SOURCE, "Erreur réseau lors de la récupération des dépôts: " + e.getMessage()); + throw new RemoteConnectorException("Erreur réseau: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(LOG_SOURCE, "Requête interrompue: " + e.getMessage()); + throw new RemoteConnectorException("Requête interrompue", e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void createRepository(String name, boolean isPrivate) throws RemoteConnectorException { + log.debug(LOG_SOURCE, "Création du dépôt Gitea: " + name + " (privé=" + isPrivate + ")"); + + String url = instanceUrl + "/api/v1/user/repos"; + + // Construction du body JSON + JSONObject body = new JSONObject(); + body.put("name", name); + body.put("private", isPrivate); + body.put("auto_init", true); // Créer avec README initial + body.put("default_branch", "main"); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "token " + token) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJSONString())) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + handleErrorResponse(response); + } + + log.info(LOG_SOURCE, "Dépôt créé avec succès: " + name); + + } catch (IOException e) { + log.error(LOG_SOURCE, "Erreur réseau lors de la création du dépôt: " + e.getMessage()); + throw new RemoteConnectorException("Erreur réseau: " + e.getMessage(), e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error(LOG_SOURCE, "Requête interrompue: " + e.getMessage()); + throw new RemoteConnectorException("Requête interrompue", e); + } + } + + /** + * Parse la réponse JSON pour extraire la liste des dépôts. + */ + private List parseRepositories(String jsonResponse) throws RemoteConnectorException { + List repos = new ArrayList<>(); + + try { + JSONArray jsonArray = (JSONArray) jsonParser.parse(jsonResponse); + + for (Object obj : jsonArray) { + JSONObject repo = (JSONObject) obj; + + String name = (String) repo.get("name"); + String cloneUrl = (String) repo.get("clone_url"); + String description = (String) repo.get("description"); + Boolean isPrivate = (Boolean) repo.get("private"); + String defaultBranch = (String) repo.get("default_branch"); + + repos.add(new RemoteRepo( + name, + cloneUrl, + description != null ? description : "", + isPrivate != null && isPrivate, + defaultBranch != null ? defaultBranch : "main" + )); + } + + log.debug(LOG_SOURCE, "Parsed " + repos.size() + " repositories"); + return repos; + + } catch (ParseException e) { + log.error(LOG_SOURCE, "Erreur de parsing JSON: " + e.getMessage()); + throw new RemoteConnectorException("Réponse JSON invalide", e); + } + } + + /** + * Gère les réponses d'erreur HTTP et lève une exception appropriée. + */ + private void handleErrorResponse(HttpResponse response) throws RemoteConnectorException { + int statusCode = response.statusCode(); + String body = response.body(); + + String errorMessage = extractErrorMessage(body); + + log.error(LOG_SOURCE, "Erreur API Gitea " + statusCode + ": " + errorMessage); + throw new RemoteConnectorException(statusCode, errorMessage); + } + + /** + * Extrait le message d'erreur du JSON de réponse Gitea. + */ + private String extractErrorMessage(String jsonResponse) { + try { + JSONObject json = (JSONObject) jsonParser.parse(jsonResponse); + String message = (String) json.get("message"); + return message != null ? message : "Erreur inconnue"; + } catch (Exception e) { + return "Erreur inconnue"; + } + } +} diff --git a/src/main/java/utils/RemoteConnector.java b/src/main/java/utils/RemoteConnector.java new file mode 100644 index 0000000..c5002ed --- /dev/null +++ b/src/main/java/utils/RemoteConnector.java @@ -0,0 +1,85 @@ +package utils; + +import java.util.List; + +/** + * Interface définissant les opérations d'un connecteur vers une plateforme Git distante. + * + *

Les implémentations de cette interface permettent d'interagir avec les APIs REST + * de GitHub, GitLab, Gitea et autres plateformes compatibles pour lister et créer + * des dépôts sans utiliser directement Git.

+ * + *

Authentification : Les connecteurs utilisent un token d'accès personnel + * fourni lors de la construction. Le token doit avoir les permissions nécessaires + * pour lire et créer des dépôts.

+ * + *

Gestion des erreurs : Toutes les méthodes peuvent lever une + * {@link RemoteConnectorException} en cas d'échec (erreur réseau, authentification + * invalide, limite de taux dépassée, etc.). Les erreurs sont loggées avant d'être + * levées.

+ * + * @see RemoteConnectorFactory + * @see RemoteConnectorException + */ +public interface RemoteConnector { + + /** + * Retourne l'identifiant de la plateforme. + * + * @return "github", "gitlab" ou "gitea" + */ + String platform(); + + /** + * Liste les dépôts appartenant à l'utilisateur authentifié. + * + *

Seuls les dépôts dont l'utilisateur est propriétaire sont retournés + * (pas les forks, ni les dépôts d'organisations par défaut).

+ * + * @return La liste des dépôts (vide si aucun dépôt ou en cas d'erreur silencieuse) + * @throws RemoteConnectorException En cas d'erreur API (auth invalide, réseau, etc.) + */ + List listRepositories() throws RemoteConnectorException; + + /** + * Crée un nouveau dépôt distant. + * + * @param name Le nom du dépôt (sans espaces) + * @param isPrivate {@code true} pour créer un dépôt privé, {@code false} pour public + * @throws RemoteConnectorException En cas d'erreur API (nom déjà utilisé, quota dépassé, etc.) + */ + void createRepository(String name, boolean isPrivate) throws RemoteConnectorException; + + /** + * Record représentant un dépôt distant. + * + * @param name Le nom du dépôt + * @param cloneUrl L'URL de clone HTTPS (ex: https://github.com/user/repo.git) + * @param description La description du dépôt (peut être vide) + * @param isPrivate {@code true} si le dépôt est privé + * @param defaultBranch Le nom de la branche par défaut (ex: "main", "master") + */ + record RemoteRepo( + String name, + String cloneUrl, + String description, + boolean isPrivate, + String defaultBranch) { + + /** + * Construit un RemoteRepo avec tous les champs. + */ + public RemoteRepo { + // Validation basique + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Le nom du dépôt ne peut pas être vide"); + } + if (cloneUrl == null || cloneUrl.isBlank()) { + throw new IllegalArgumentException("L'URL de clone ne peut pas être vide"); + } + // Normaliser les valeurs nulles + description = description != null ? description : ""; + defaultBranch = defaultBranch != null && !defaultBranch.isBlank() ? defaultBranch : "main"; + } + } +} diff --git a/src/main/java/utils/RemoteConnectorException.java b/src/main/java/utils/RemoteConnectorException.java new file mode 100644 index 0000000..e04ab4e --- /dev/null +++ b/src/main/java/utils/RemoteConnectorException.java @@ -0,0 +1,105 @@ +package utils; + +/** + * Exception levée lors d'une erreur avec un connecteur distant (GitHub, GitLab, Gitea). + * + *

Cette exception encapsule les erreurs HTTP et les erreurs API spécifiques, + * fournissant un code de statut HTTP et un message d'erreur descriptif que l'UI + * peut afficher à l'utilisateur.

+ */ +public class RemoteConnectorException extends Exception { + + private static final long serialVersionUID = 1L; + + private final int statusCode; + private final String apiMessage; + + /** + * Construit une exception avec un message et une cause. + * + * @param message Le message d'erreur + * @param cause La cause originale (peut être null) + */ + public RemoteConnectorException(String message, Throwable cause) { + super(message, cause); + this.statusCode = 0; + this.apiMessage = null; + } + + /** + * Construit une exception avec un code HTTP, un message d'erreur de l'API et une cause. + * + * @param statusCode Le code de statut HTTP (401, 403, 404, 429, etc.) + * @param apiMessage Le message d'erreur retourné par l'API + * @param cause La cause originale (peut être null) + */ + public RemoteConnectorException(int statusCode, String apiMessage, Throwable cause) { + super(buildMessage(statusCode, apiMessage), cause); + this.statusCode = statusCode; + this.apiMessage = apiMessage; + } + + /** + * Construit une exception avec un code HTTP et un message d'erreur de l'API. + * + * @param statusCode Le code de statut HTTP + * @param apiMessage Le message d'erreur retourné par l'API + */ + public RemoteConnectorException(int statusCode, String apiMessage) { + this(statusCode, apiMessage, null); + } + + /** + * Obtient le code de statut HTTP associé à l'erreur. + * + * @return Le code HTTP (0 si non applicable) + */ + public int getStatusCode() { + return statusCode; + } + + /** + * Obtient le message d'erreur retourné par l'API. + * + * @return Le message de l'API (null si non disponible) + */ + public String getApiMessage() { + return apiMessage; + } + + /** + * Construit un message d'erreur formaté à partir du code HTTP et du message API. + */ + private static String buildMessage(int statusCode, String apiMessage) { + StringBuilder sb = new StringBuilder(); + sb.append("Erreur API "); + + switch (statusCode) { + case 401: + sb.append("(401 Unauthorized): Authentification invalide"); + break; + case 403: + sb.append("(403 Forbidden): Accès refusé"); + break; + case 404: + sb.append("(404 Not Found): Ressource non trouvée"); + break; + case 429: + sb.append("(429 Too Many Requests): Limite de taux dépassée"); + break; + case 500: + case 502: + case 503: + sb.append("(").append(statusCode).append("): Erreur serveur"); + break; + default: + sb.append("(").append(statusCode).append(")"); + } + + if (apiMessage != null && !apiMessage.isBlank()) { + sb.append(" - ").append(apiMessage); + } + + return sb.toString(); + } +} diff --git a/src/main/java/utils/RemoteConnectorFactory.java b/src/main/java/utils/RemoteConnectorFactory.java new file mode 100644 index 0000000..8426ca0 --- /dev/null +++ b/src/main/java/utils/RemoteConnectorFactory.java @@ -0,0 +1,132 @@ +package utils; + +import org.eclipse.jgit.transport.URIish; + +import java.net.URISyntaxException; + +/** + * Factory statique pour créer des instances de {@link RemoteConnector} à partir d'une URL Git. + * + *

Cette factory détecte automatiquement la plateforme (GitHub, GitLab, Gitea) en analysant + * l'URL du dépôt distant et instancie le connecteur approprié.

+ * + *

Formats d'URL supportés :

+ *
    + *
  • HTTPS: {@code https://github.com/user/repo.git}
  • + *
  • SSH: {@code git@github.com:user/repo.git}
  • + *
  • SSH avec protocole: {@code ssh://git@github.com/user/repo.git}
  • + *
+ * + *

Détection de plateforme :

+ *
    + *
  • GitHub: détecté si l'hôte contient "github.com"
  • + *
  • GitLab: détecté si l'hôte contient "gitlab.com"
  • + *
  • Gitea: assumé pour toute autre URL (instance custom)
  • + *
+ * + * @see RemoteConnector + */ +public final class RemoteConnectorFactory { + + private static final LogService log = LogService.getInstance(); + private static final String LOG_SOURCE = "RemoteConnectorFactory"; + + // Empêcher l'instanciation + private RemoteConnectorFactory() { + } + + /** + * Crée un connecteur approprié pour l'URL fournie. + * + *

L'URL est analysée pour déterminer la plateforme, puis le connecteur + * correspondant est instancié avec le token fourni.

+ * + * @param remoteUrl L'URL du dépôt distant (HTTPS ou SSH) + * @param token Le token d'accès personnel pour l'authentification + * @return Une instance de RemoteConnector, ou {@code null} si la plateforme n'est pas supportée + */ + public static RemoteConnector create(String remoteUrl, String token) { + if (remoteUrl == null || remoteUrl.isBlank()) { + log.warn(LOG_SOURCE, "URL de dépôt vide"); + return null; + } + + if (token == null || token.isBlank()) { + log.warn(LOG_SOURCE, "Token d'authentification vide"); + return null; + } + + try { + // Parser l'URL avec JGit URIish pour supporter tous les formats Git + URIish uri = new URIish(remoteUrl); + String host = uri.getHost(); + + if (host == null || host.isBlank()) { + log.warn(LOG_SOURCE, "Impossible d'extraire l'hôte de l'URL: " + remoteUrl); + return null; + } + + host = host.toLowerCase(); + + // Détection de la plateforme par le nom d'hôte + if (host.contains("github.com")) { + log.debug(LOG_SOURCE, "Détection de GitHub pour: " + host); + return new GitHubConnector(token); + } else if (host.contains("gitlab.com")) { + log.debug(LOG_SOURCE, "Détection de GitLab public pour: " + host); + return new GitLabConnector("https://gitlab.com", token); + } else if (host.contains("gitlab")) { + // Instance GitLab self-hosted (contient "gitlab" dans le domaine) + String instanceUrl = uri.getScheme() + "://" + host; + log.debug(LOG_SOURCE, "Détection de GitLab self-hosted: " + instanceUrl); + return new GitLabConnector(instanceUrl, token); + } else { + // Par défaut, on assume Gitea pour les instances custom + String instanceUrl = uri.getScheme() + "://" + host; + if (uri.getPort() > 0) { + instanceUrl += ":" + uri.getPort(); + } + log.debug(LOG_SOURCE, "Détection de Gitea (ou instance custom): " + instanceUrl); + return new GiteaConnector(instanceUrl, token); + } + + } catch (URISyntaxException e) { + log.error(LOG_SOURCE, "URL invalide: " + remoteUrl + " - " + e.getMessage()); + return null; + } + } + + /** + * Détecte le type de plateforme à partir d'une URL sans créer de connecteur. + * + * @param remoteUrl L'URL du dépôt distant + * @return "github", "gitlab", "gitea" ou {@code null} si non détecté + */ + public static String detectPlatform(String remoteUrl) { + if (remoteUrl == null || remoteUrl.isBlank()) { + return null; + } + + try { + URIish uri = new URIish(remoteUrl); + String host = uri.getHost(); + + if (host == null || host.isBlank()) { + return null; + } + + host = host.toLowerCase(); + + if (host.contains("github.com")) { + return "github"; + } else if (host.contains("gitlab")) { + return "gitlab"; + } else { + return "gitea"; + } + + } catch (URISyntaxException e) { + return null; + } + } +} diff --git a/src/test/java/utils/RemoteConnectorExceptionTest.java b/src/test/java/utils/RemoteConnectorExceptionTest.java new file mode 100644 index 0000000..1997ca0 --- /dev/null +++ b/src/test/java/utils/RemoteConnectorExceptionTest.java @@ -0,0 +1,101 @@ +package utils; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitaires pour RemoteConnectorException. + */ +class RemoteConnectorExceptionTest { + + @Test + void testConstructorWithMessageAndCause() { + Exception cause = new RuntimeException("root cause"); + RemoteConnectorException ex = new RemoteConnectorException("Test error", cause); + + assertEquals("Test error", ex.getMessage()); + assertEquals(cause, ex.getCause()); + assertEquals(0, ex.getStatusCode()); + assertNull(ex.getApiMessage()); + } + + @Test + void testConstructorWithStatusCodeAndMessage() { + RemoteConnectorException ex = new RemoteConnectorException(404, "Resource not found"); + + assertTrue(ex.getMessage().contains("404")); + assertTrue(ex.getMessage().contains("Resource not found")); + assertEquals(404, ex.getStatusCode()); + assertEquals("Resource not found", ex.getApiMessage()); + } + + @Test + void testConstructorWithStatusCodeMessageAndCause() { + Exception cause = new RuntimeException("network error"); + RemoteConnectorException ex = new RemoteConnectorException(500, "Server error", cause); + + assertTrue(ex.getMessage().contains("500")); + assertTrue(ex.getMessage().contains("Server error")); + assertEquals(500, ex.getStatusCode()); + assertEquals("Server error", ex.getApiMessage()); + assertEquals(cause, ex.getCause()); + } + + @Test + void testUnauthorizedMessage() { + RemoteConnectorException ex = new RemoteConnectorException(401, "Invalid token"); + assertTrue(ex.getMessage().contains("401 Unauthorized")); + assertTrue(ex.getMessage().contains("Invalid token")); + } + + @Test + void testForbiddenMessage() { + RemoteConnectorException ex = new RemoteConnectorException(403, "Access denied"); + assertTrue(ex.getMessage().contains("403 Forbidden")); + assertTrue(ex.getMessage().contains("Access denied")); + } + + @Test + void testNotFoundMessage() { + RemoteConnectorException ex = new RemoteConnectorException(404, "Repository not found"); + assertTrue(ex.getMessage().contains("404 Not Found")); + assertTrue(ex.getMessage().contains("Repository not found")); + } + + @Test + void testRateLimitMessage() { + RemoteConnectorException ex = new RemoteConnectorException(429, "Rate limit exceeded"); + assertTrue(ex.getMessage().contains("429 Too Many Requests")); + assertTrue(ex.getMessage().contains("Rate limit exceeded")); + } + + @Test + void testServerErrorMessage() { + RemoteConnectorException ex = new RemoteConnectorException(500, "Internal error"); + assertTrue(ex.getMessage().contains("500")); + assertTrue(ex.getMessage().contains("Erreur serveur")); + assertTrue(ex.getMessage().contains("Internal error")); + } + + @Test + void testUnknownStatusCode() { + RemoteConnectorException ex = new RemoteConnectorException(418, "I'm a teapot"); + assertTrue(ex.getMessage().contains("418")); + assertTrue(ex.getMessage().contains("I'm a teapot")); + } + + @Test + void testNullApiMessage() { + RemoteConnectorException ex = new RemoteConnectorException(404, null); + assertNotNull(ex.getMessage()); + assertTrue(ex.getMessage().contains("404")); + assertNull(ex.getApiMessage()); + } + + @Test + void testBlankApiMessage() { + RemoteConnectorException ex = new RemoteConnectorException(404, " "); + assertNotNull(ex.getMessage()); + assertTrue(ex.getMessage().contains("404")); + } +} diff --git a/src/test/java/utils/RemoteConnectorFactoryTest.java b/src/test/java/utils/RemoteConnectorFactoryTest.java new file mode 100644 index 0000000..757e75e --- /dev/null +++ b/src/test/java/utils/RemoteConnectorFactoryTest.java @@ -0,0 +1,105 @@ +package utils; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitaires pour RemoteConnectorFactory. + */ +class RemoteConnectorFactoryTest { + + @Test + void testDetectGitHub() { + assertEquals("github", RemoteConnectorFactory.detectPlatform("https://github.com/user/repo.git")); + assertEquals("github", RemoteConnectorFactory.detectPlatform("git@github.com:user/repo.git")); + assertEquals("github", RemoteConnectorFactory.detectPlatform("ssh://git@github.com/user/repo.git")); + } + + @Test + void testDetectGitLabPublic() { + assertEquals("gitlab", RemoteConnectorFactory.detectPlatform("https://gitlab.com/user/repo.git")); + assertEquals("gitlab", RemoteConnectorFactory.detectPlatform("git@gitlab.com:user/repo.git")); + } + + @Test + void testDetectGitLabSelfHosted() { + assertEquals("gitlab", RemoteConnectorFactory.detectPlatform("https://gitlab.example.com/user/repo.git")); + assertEquals("gitlab", RemoteConnectorFactory.detectPlatform("https://my-gitlab.internal/user/repo.git")); + } + + @Test + void testDetectGitea() { + assertEquals("gitea", RemoteConnectorFactory.detectPlatform("https://gitea.example.com/user/repo.git")); + assertEquals("gitea", RemoteConnectorFactory.detectPlatform("https://code.company.com/user/repo.git")); + } + + @Test + void testDetectPlatformWithInvalidUrl() { + assertNull(RemoteConnectorFactory.detectPlatform("")); + assertNull(RemoteConnectorFactory.detectPlatform(null)); + assertNull(RemoteConnectorFactory.detectPlatform("not-a-url")); + } + + @Test + void testCreateGitHubConnector() { + RemoteConnector connector = RemoteConnectorFactory.create("https://github.com/user/repo.git", "token123"); + assertNotNull(connector); + assertTrue(connector instanceof GitHubConnector); + assertEquals("github", connector.platform()); + } + + @Test + void testCreateGitLabConnector() { + RemoteConnector connector = RemoteConnectorFactory.create("https://gitlab.com/user/repo.git", "token123"); + assertNotNull(connector); + assertTrue(connector instanceof GitLabConnector); + assertEquals("gitlab", connector.platform()); + } + + @Test + void testCreateGitLabSelfHostedConnector() { + RemoteConnector connector = RemoteConnectorFactory.create("https://gitlab.example.com/user/repo.git", "token123"); + assertNotNull(connector); + assertTrue(connector instanceof GitLabConnector); + assertEquals("gitlab", connector.platform()); + } + + @Test + void testCreateGiteaConnector() { + RemoteConnector connector = RemoteConnectorFactory.create("https://gitea.example.com/user/repo.git", "token123"); + assertNotNull(connector); + assertTrue(connector instanceof GiteaConnector); + assertEquals("gitea", connector.platform()); + } + + @Test + void testCreateWithEmptyUrl() { + assertNull(RemoteConnectorFactory.create("", "token123")); + assertNull(RemoteConnectorFactory.create(null, "token123")); + } + + @Test + void testCreateWithEmptyToken() { + assertNull(RemoteConnectorFactory.create("https://github.com/user/repo.git", "")); + assertNull(RemoteConnectorFactory.create("https://github.com/user/repo.git", null)); + } + + @Test + void testCreateWithInvalidUrl() { + assertNull(RemoteConnectorFactory.create("not-a-valid-url", "token123")); + } + + @Test + void testGitHubSSHUrl() { + RemoteConnector connector = RemoteConnectorFactory.create("git@github.com:user/repo.git", "token123"); + assertNotNull(connector); + assertEquals("github", connector.platform()); + } + + @Test + void testGitLabSSHUrl() { + RemoteConnector connector = RemoteConnectorFactory.create("git@gitlab.com:user/repo.git", "token123"); + assertNotNull(connector); + assertEquals("gitlab", connector.platform()); + } +} diff --git a/src/test/java/utils/RemoteRepoTest.java b/src/test/java/utils/RemoteRepoTest.java new file mode 100644 index 0000000..98c7498 --- /dev/null +++ b/src/test/java/utils/RemoteRepoTest.java @@ -0,0 +1,157 @@ +package utils; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitaires pour le record RemoteRepo. + */ +class RemoteRepoTest { + + @Test + void testValidRemoteRepo() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "my-repo", + "https://github.com/user/my-repo.git", + "A test repository", + true, + "main" + ); + + assertEquals("my-repo", repo.name()); + assertEquals("https://github.com/user/my-repo.git", repo.cloneUrl()); + assertEquals("A test repository", repo.description()); + assertTrue(repo.isPrivate()); + assertEquals("main", repo.defaultBranch()); + } + + @Test + void testNullDescription() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "my-repo", + "https://github.com/user/my-repo.git", + null, + false, + "main" + ); + + assertEquals("", repo.description()); + } + + @Test + void testNullDefaultBranch() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "my-repo", + "https://github.com/user/my-repo.git", + "Description", + false, + null + ); + + assertEquals("main", repo.defaultBranch()); + } + + @Test + void testBlankDefaultBranch() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "my-repo", + "https://github.com/user/my-repo.git", + "Description", + false, + " " + ); + + assertEquals("main", repo.defaultBranch()); + } + + @Test + void testNullName() { + assertThrows(IllegalArgumentException.class, () -> { + new RemoteConnector.RemoteRepo( + null, + "https://github.com/user/repo.git", + "Description", + false, + "main" + ); + }); + } + + @Test + void testBlankName() { + assertThrows(IllegalArgumentException.class, () -> { + new RemoteConnector.RemoteRepo( + " ", + "https://github.com/user/repo.git", + "Description", + false, + "main" + ); + }); + } + + @Test + void testNullCloneUrl() { + assertThrows(IllegalArgumentException.class, () -> { + new RemoteConnector.RemoteRepo( + "my-repo", + null, + "Description", + false, + "main" + ); + }); + } + + @Test + void testBlankCloneUrl() { + assertThrows(IllegalArgumentException.class, () -> { + new RemoteConnector.RemoteRepo( + "my-repo", + " ", + "Description", + false, + "main" + ); + }); + } + + @Test + void testPublicRepository() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "public-repo", + "https://github.com/user/public-repo.git", + "Public repository", + false, + "main" + ); + + assertFalse(repo.isPrivate()); + } + + @Test + void testPrivateRepository() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "private-repo", + "https://github.com/user/private-repo.git", + "Private repository", + true, + "main" + ); + + assertTrue(repo.isPrivate()); + } + + @Test + void testCustomDefaultBranch() { + RemoteConnector.RemoteRepo repo = new RemoteConnector.RemoteRepo( + "my-repo", + "https://github.com/user/my-repo.git", + "Description", + false, + "develop" + ); + + assertEquals("develop", repo.defaultBranch()); + } +} From 42aaa516e49d28faa2290f606817ef7ea7490224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delorme?= Date: Tue, 19 May 2026 00:27:50 +0200 Subject: [PATCH 3/3] prepare stage 3 --- .../reqs/req-68-git-client-and-connector.md | 99 ++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/docs/reqs/req-68-git-client-and-connector.md b/src/docs/reqs/req-68-git-client-and-connector.md index ea202ce..0388395 100644 --- a/src/docs/reqs/req-68-git-client-and-connector.md +++ b/src/docs/reqs/req-68-git-client-and-connector.md @@ -409,8 +409,103 @@ No additional native binaries are required. JGit bundles all transitive dependen - **Authentication**: Token-based only (stored in AppConfig, chmod 600) - **Scope**: Repository operations only; no issues, PRs, CI status (deferred to future Stage 4+) -3. stage 3 - UI integration - - [ ] integrate the RemoteConnector usage into git dialogs and UI. +3. Stage 3 - UI Integration + + **Goal**: Integrate RemoteConnector functionality into MarkNote's Git UI to enable browsing remote repositories, creating new repositories, and selecting remotes from GitHub/GitLab/Gitea. + + **Context**: + - Stage 1 Completed: GitService + full Git UI (dialogs, toolbar, menus) + - Stage 2 Completed: RemoteConnector backend (GitHub/GitLab/Gitea APIs) + + **Architecture Decisions**: + 1. **Integration points**: Add "Browse Repositories" and "Create Repository" dialogs accessible from AddRemoteDialog, GitOptionsTab, and root context menu + 2. **Token management**: Reuse token from AddRemoteDialog/Options (simpler, most users use same token for Git and API) + 3. **Error handling**: Inline error labels for non-critical errors (rate limit, 404), Alert dialogs for auth failures + + **Phase 1: Repository Browser Dialog (NEW)** + + - [ ] Create `RemoteRepositoryBrowserDialog.java` in `src/main/java/ui/` (~300 lines) + - UI components: ComboBox (platform), PasswordField (token), Button (Test/Refresh), ListView (repos with custom cell renderer), Label (status) + - Constructor: `RemoteRepositoryBrowserDialog(Window owner, String platform, String token)` + - Behavior: Auto-load repos on open, Test button validates token, Refresh re-fetches list, Select/double-click returns RemoteRepo + - Methods: `loadRepositories()`, `handleTestConnection()`, `handleRefresh()`, `handleSelect()`, `getSelectedRepository()` + - Error handling: 401/403 → "Invalid token", 429 → "Rate limit exceeded", network → "Connection failed", empty → "No repositories found" + - Thread safety: API calls in background thread, UI updates via Platform.runLater + + **Phase 2: Create Repository Dialog (NEW)** + + - [ ] Create `CreateRemoteRepositoryDialog.java` in `src/main/java/ui/` (~250 lines) + - UI components: ComboBox (platform), PasswordField (token), Button (Test), TextField (name with validation), TextField (description), CheckBox (private, default true), CheckBox (init README, default true), Label (status) + - Constructor: `CreateRemoteRepositoryDialog(Window owner, String platform, String token)` + - Validation: Enable Create button only if valid name (alphanumeric + dash/underscore, no spaces) + - Methods: `validateForm()`, `handleTestConnection()`, `handleCreate()`, `getCreatedRepository()` + - Error handling: 401/403 → "Authentication failed", 409 → "Repository name already exists", 422 → "Invalid repository name" + + **Phase 3: Enhance AddRemoteDialog** + + - [ ] Modify `src/main/java/ui/AddRemoteDialog.java` (add ~80 lines) + - Add "Browse…" button next to Remote URL field (enabled when token entered) + - Opens RemoteRepositoryBrowserDialog with detected platform + - On selection: fills URL field with repo's cloneUrl + - Add "Create New…" button next to Remote URL field (enabled when token entered) + - Opens CreateRemoteRepositoryDialog with detected platform + - On success: fills URL field with created repo's cloneUrl + - Auto-detect platform from URL using `RemoteConnectorFactory.detectPlatform()` + - Display hint: "Detected: GitHub" / "Detected: GitLab" / "Detected: Gitea" + - Implementation: Button disable bindings to tokenField, handlers `handleBrowseRepositories()` and `handleCreateRepository()` + + **Phase 4: Enhance GitOptionsTab** + + - [ ] Modify `src/main/java/ui/GitOptionsTab.java` (add ~60 lines) + - Add "Remote Repositories" section below credentials + - Shows current remote URL if configured + - Buttons: "Browse Repositories…" and "Create Repository…" + - Implementation: `buildRemoteRepoSection()`, handlers `handleBrowseFromOptions()` and `handleCreateFromOptions()` + - Behavior: Browse/Create may update config or offer to set as remote after success + + **Phase 5: Context Menu Integration** + + - [ ] Modify `src/main/java/ui/ProjectExplorerPanel.java` (add ~40 lines) + - Add "⊕ Create Remote Repository…" to root folder context menu (when no remote configured) + - Implementation: Handler `handleCreateRemoteRepository()` checks token in AppConfig, opens CreateRemoteRepositoryDialog, offers to set as remote on success + - Workflow: Create repo → confirmation dialog → `gitService.addRemote(repo.cloneUrl())` → success message + + **Phase 6: Testing** + + - [ ] Create unit tests in `src/test/java/ui/` + - `RemoteRepositoryBrowserDialogTest.java` (~200 lines): Test repo list rendering, selection logic, error display, mock RemoteConnector responses + - `CreateRemoteRepositoryDialogTest.java` (~150 lines): Test form validation, create button enable/disable, success/error scenarios, mock RemoteConnector.createRepository() + + - [ ] Manual testing scenarios + - AddRemoteDialog integration: Enter token → Browse enables → lists repos → select → URL populated + - AddRemoteDialog integration: Click Create New → dialog opens → repo created → URL filled + - GitOptionsTab integration: Browse/Create buttons work, token validation + - Context menu: Right-click root → Create Remote Repository → confirm → remote configured → push works + - Error scenarios: Invalid token, rate limit, network error, empty repo list + + - [ ] Integration testing workflows + - **Workflow 1 (New project)**: Init Git → commit → right-click root → Create Remote Repository → enter token → create private repo → confirm set as remote → push succeeds + - **Workflow 2 (Existing repo)**: Open project (no remote) → Options → Git → Browse Repositories → select → confirm → pull/push work + - **Workflow 3 (Token reuse)**: Configure token in Options → AddRemoteDialog auto-uses token → Browse/Create work without re-entering + + **Files Summary**: + - **New files**: RemoteRepositoryBrowserDialog.java (~300 lines), CreateRemoteRepositoryDialog.java (~250 lines) + - **Modified files**: AddRemoteDialog.java (+80 lines), GitOptionsTab.java (+60 lines), ProjectExplorerPanel.java (+40 lines) + - **Test files**: RemoteRepositoryBrowserDialogTest.java (~200 lines), CreateRemoteRepositoryDialogTest.java (~150 lines) + - **Total estimate**: ~1080 lines + + **Risks & Mitigations**: + - Token security → Use PasswordField (masked), never log tokens + - API rate limits → Show clear error on 429, cache repo lists for 5 minutes + - Network latency → All calls in background threads, show loading spinners + - Platform differences → Already handled in Stage 2 connectors, test each platform + - User confusion → Clear labels, tooltips, inline help + + **Future Enhancements (Stage 4+)**: + - Show repo description/stats in browser dialog + - Filter repos by visibility, search by name + - Clone repos directly from browser dialog + - CI/CD status indicators, GitHub/GitLab issues integration, PR creation > [!IMPORTANT] GitService class > The Git client is a big feature; create it as a dedicated `GitService` class and keep all JGit calls inside it. The UI layer must never import JGit directly.