Metadata-Version: 2.4
Name: denary
Version: 0.0.128
Summary: A curses-based text editor with syntax highlighting
License: MIT
License-File: LICENSE
Keywords: editor,tui,curses,syntax-highlighting,terminal
Author: Jordaly Suriel
Requires-Python: >=3.12,<3.15
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: Pygments (>=2.20)
Requires-Dist: pathspec (>=1.0.4)
Requires-Dist: platformdirs (>=4.9.6)
Requires-Dist: windows-curses (>=2.4.2) ; sys_platform == "win32"
Project-URL: Homepage, https://gitlab.com/jordaly/editor
Project-URL: Repository, https://gitlab.com/jordaly/editor
Description-Content-Type: text/markdown

![](./tests.svg)
![](./mypy.svg)
![](./coverage.svg)

## Editor Documentation
```
 ,gggggggggggg,
dP"""88""""""Y8b,
Yb,  88       `8b,
 `"  88        `8b
     88         Y8
     88         d8  ,ggg,    ,ggg,,ggg,     ,gggg,gg   ,gggggg,  gg     gg
     88        ,8P i8" "8i  ,8" "8P" "8,   dP"  "Y8I   dP""""8I  I8     8I
     88       ,8P' I8, ,8I  I8   8I   8I  i8'    ,8I  ,8'    8I  I8,   ,8I
     88______,dP'  `YbadP' ,dP   8I   Yb,,d8,   ,d8b,,dP     Y8,,d8b, ,d8I
    888888888P"   888P"Y8888P'   8I   `Y8P"Y8888P"`Y88P      `Y8P""Y88P"888
                                                                      ,d8I'
                                                                    ,dP'8I
                                                                   ,8"  8I
                                                                   I8   8I
                                                                   `8, ,8I
                                                                    `Y8P"
```

A modal terminal editor built on curses with a command-driven,
event-based architecture and chunked text storage.

Includes syntax highlighting, multi-buffer management,
registers and markers, undo/redo history, Git integration,
async LSP and Debug Adapter Protocol support, subprocess execution, and HTTP
requests.

## Architecture Documentation

- [System architecture](docs/architecture.html) — component boundaries, state ownership, lifecycle, and integrations.
- [Data flow](docs/data-flow.html) — interactive traces from input and file I/O through buffers, services, and rendering.

## Debug Adapter Protocol

Denary can launch any stdio-based DAP adapter through named profiles in the
user `init.py`. For example, a Python profile using debugpy is:

```python
DAP_CONFIG = {
    "Python: current file": {
        "cmd": ["python", "-m", "debugpy.adapter"],
        "request": "launch",
        "arguments": {
            "program": "${file}",
            "cwd": "${workspaceFolder}",
        },
    },
}
```

`${file}`, `${fileDirname}`, and `${workspaceFolder}` are expanded throughout
the profile. The adapter itself must be installed separately.

Core debugger commands are `debug_start`, `debug_stop`,
`debug_toggle_breakpoint`, `debug_continue`, `debug_pause`, `debug_next`,
`debug_step_in`, `debug_step_out`, `debug_threads`, `debug_stack`,
`debug_variables`, and `debug_output`. Breakpoints are restored from the
project session file.


## Installation

### Install via pip (Recommended)

If your package is published:

```bash
pip install denary
```

Then run:

```bash
denary
```

Or open a file directly:

```bash
denary main.py
```

---

### Install from Source

Clone the repository:

```bash
git clone https://gitlab.com/jordaly/editor.git denary
cd denary
```

Using **Poetry**:

```bash
poetry install
poetry run python -m denary
```

Or using plain pip:

```bash
pip install -e .
denary
```

### Recommended (Better): Alias That Runs Denary Directly

Instead of activating the venv every time, just point the alias to the venv’s binary.

Assume your venv is here:

```bash
~/venvs/denary_venv
```

Edit your `~/.bashrc`:

```bash
nano ~/.bashrc
```

Add this line:

```bash
alias denary="~/venvs/denary_venv/bin/denary"
```

Then reload:

```bash
source ~/.bashrc
```

Now you can run:

```bash
denary
denary myfile.py
```

## Quick Start

### Open a File

```bash
denary myfile.py
```

If the file does not exist, it will be created.

## AI Chat

Denary can open a persistent, project-specific Ollama conversation beside the
current code pane. Start Ollama, install at least one model, then run:

```text
:ai_chat
```

The chat pane streams responses and keeps its conversation history outside the
project in Denary's platform data directory. It sends conversation history but
does not include editor code unless you explicitly attach the current selection
or buffer.

| Chat key | Action |
| --- | --- |
| `Enter` / `i` | Enter the multiline composer |
| `Ctrl+G` | Send the composed message |
| `Enter` | Insert a newline while composing |
| `Esc` | Return to chat navigation while retaining the draft |
| `Ctrl+P` | Paste the system or Denary clipboard into the composer |
| `a` | Attach the current selection, current buffer, or any open buffer |
| `x` | Detach a pending attachment before sending |
| `n` / `h` | Start a new chat / open chat history |
| `m` | Choose an AI provider/model |
| `c` / `r` | Cancel the active response / retry the last turn |
| `R` / `d` | Rename / delete the active conversation |
| `j`, `k`, arrows, page keys, `g`, `G` | Move through the transcript |
| `v` / `y` | Start or end a line selection / copy selected chat text |
| `q` | Close the chat pane without deleting its history |

Ollama keeps its native API workflow, but its server is configurable through
`static_config.json`:

```json
{"ollama_url": "http://localhost:11434"}
```

For a remote Ollama server, replace the URL with its reachable HTTP address.
External OpenAI-compatible providers can be added without provider-specific
code. `api_key_env` is only the name of the environment variable containing
the API key; it is not the key itself and should not include `Bearer`:

```json
{
  "ai_providers": {
    "cloud": {
      "type": "openai_compatible",
      "base_url": "https://api.example.com/v1",
      "api_key_env": "CLOUD_API_KEY",
      "context_window": 8192,
      "models": ["coding-model"]
    }
  },
  "ai_roles": {
    "chat": {"provider": "cloud", "model": "coding-model"},
    "autocomplete": {"provider": "ollama", "model": "qwen-code"}
  }
}
```

For the example above, set the key in your shell before starting Denary:

```bash
export CLOUD_API_KEY="your-secret-key"
```

The editor reads `CLOUD_API_KEY` and sends it as `Authorization: Bearer ...`.
Do not store the actual API key in `static_config.json`.

The `chat` role is used by chat and non-inline AI commands. The
`autocomplete` role controls inline completion. Existing `model` and
`ai_autocomplete_model` settings continue to work as Ollama defaults.

Related commands use the `ai_chat_*` prefix and are listed by `:help`.

## Key Bindings

## Arrow & Basic Movement

| Shortcut       | Action                              |
| -------------- | ----------------------------------- |
| **Right**      | Move cursor right                   |
| **Left**       | Move cursor left                    |
| **Up**         | Move cursor up                      |
| **Down**       | Move cursor down                    |
| **PageUp**     | Page up                             |
| **PageDown**   | Page down                           |
| **Ctrl+Right** | Jump right (word/semantic move)     |
| **Ctrl+Left**  | Jump left (word/semantic move)      |
| **Ctrl+Up**    | Scroll / move up (mode dependent)   |
| **Ctrl+Down**  | Scroll / move down (mode dependent) |

---

## Vim-Style Normal Mode Navigation

| Shortcut  | Action                           |
| --------- | -------------------------------- |
| **h**     | Move left                        |
| **j**     | Move down                        |
| **k**     | Move up                          |
| **l**     | Move right                       |
| **0**     | Jump to start of line            |
| **$**     | Jump to end of line              |
| **w / W** | Move forward by word             |
| **b / B** | Move backward by word            |
| **e / E** | Move to end of word              |
| **f / F** | Find character forward/backward  |
| **g**     | Go command prefix                |
| **G**     | Go to bottom                     |
| **H**     | Jump to top of visible window    |
| **L**     | Jump to bottom of visible window |
| **%**     | Jump to matching bracket         |

---

## Insert & Editing (Normal Mode)

| Shortcut | Action                    |
| -------- | ------------------------- |
| **i**    | Enter insert mode         |
| **a**    | Append after cursor       |
| **A**    | Append at end of line     |
| **o**    | Insert new line below     |
| **O**    | Insert new line above     |
| **x**    | Delete character          |
| **d**    | Delete (operator)         |
| **c**    | Change (operator)         |
| **y**    | Yank (copy)               |
| **p**    | Paste after               |
| **P**    | Paste before              |
| **u**    | Undo                      |
| **U**    | Undo line / extended undo |
| **J**    | Join lines                |
| **>**    | Indent right              |
| **<**    | Indent left               |

---

## Search & Repeat

| Shortcut | Action                   |
| -------- | ------------------------ |
| **/**    | Search forward           |
| **n**    | Next match               |
| **N**    | Previous match           |
| *****    | Search word under cursor |

---

## Marks & Registers

| Shortcut | Action                          |
| -------- | ------------------------------- |
| **m**    | Set marker                      |
| **'**    | Jump to marker (linewise)       |
| **`**    | Jump to marker (exact position) |
| **@**    | Execute macro                   |
| **q**    | Start/stop macro recording      |

---

## LSP / Diagnostics (Normal Mode)

| Shortcut   | Action                      |
| ---------- | --------------------------- |
| **K**      | Show LSP hover / definition |
| **;**      | LSP-related navigation      |
| **Ctrl+D** | Diagnostics-related action  |

(Exact behavior depends on your handler implementations.)

---

## Alt-Based Commands

| Shortcut              | Action                      |
| --------------------- | --------------------------- |
| **Alt+;**             | Toggle selection mode       |
| **Alt+Right / Alt+.** | Indent right                |
| **Alt+Left / Alt+,**  | Indent left                 |
| **Alt+/**             | Alternative slash action    |
| **Alt+h / Alt+l**     | Alternative horizontal move |
| **Alt+j / Alt+k**     | Move lines or cursor        |
| **Alt+Up / Alt+Down** | Scroll view                 |
| **Alt+9 / Alt+0**     | Jump line start/end         |
| **Alt+w**             | File search                 |
| **Alt+r**             | Custom action               |
| **Alt+i / Alt+o**     | Punctuation jump            |
| **Alt+[**             | Bracket-related action      |
| **Alt+'**             | Quote-related action        |

---

## Shift Selection

| Shortcut             | Action                   |
| -------------------- | ------------------------ |
| **Shift+Right**      | Expand selection right   |
| **Shift+Left**       | Expand selection left    |
| **Shift+Up**         | Expand selection up      |
| **Shift+Down**       | Expand selection down    |
| **Ctrl+Shift+Right** | Expand selection by word |
| **Ctrl+Shift+Left**  | Expand selection by word |

---

## Misc

| Shortcut              | Action                    |
| --------------------- | ------------------------- |
| **Ctrl+A**            | Select all                |
| **Ctrl+P**            | Previous buffer           |
| **Ctrl+X**            | Cut / special action      |
| **Ctrl+O**            | Open file                 |
| **Ctrl+I**            | Jump forward in jump list |
| **Q**                 | Quit variant              |
| **PageUp / PageDown** | Scroll                    |

## Commands

### Buffer Actions

| Command                           | Description         |
| --------------------------------- | ------------------- |
| `h` / `help`                      | Show help           |
| `q`                               | Exit editor         |
| `w`                               | Save current buffer |
| `saveas [path]`                   | Save under a new path |
| `wall` / `wa`                     | Save all modified buffers |
| `wq`                              | Save and close current buffer |
| `wqa`                             | Save all modified buffers and exit |
| `ls`                              | List open buffers   |
| `open`                            | Open a file         |
| `bufnew`                          | Create new buffer   |
| `nbuf` / `nextbuf` / `next` / `n` | Next buffer         |
| `pbuf` / `prevbuf` / `prev` / `p` | Previous buffer     |
| `delbuf` / `d`                    | Delete buffer       |
| `ff`                              | File search         |
| `ffr`                             | New file search     |
| `fb`                              | Buffer picker       |
| `ex`                              | File explorer       |

---

### UI & Behavior

| Command         | Description          |
| --------------- | -------------------- |
| `themes`        | Change editor theme  |
| `ln`            | Toggle line numbers  |
| `rln`           | Toggle relative line numbers |
| `wordwrap` / `ww` | Toggle soft word wrapping |
| `gch`           | Toggle Git highlight |
| `scrolloff <n>` | Set scroll offset    |
| `tabsize <n>`   | Set current buffer tab width |
| `indent <style>` | Use spaces or tabs for future indentation |
| `indentdefaults <style> <n>` | Persist indentation defaults for new files |
| `retab [style] [n]` | Convert leading indentation without changing visual width |

---

### Markers

| Command        | Description                   |
| -------------- | ----------------------------- |
| `m` / `marker` | Add local/global marker       |
| `pm`           | Pick marker                   |
| `dm`           | Delete markers in this buffer |
| `dgm`          | Delete global markers         |
| `dpm`          | Delete specific marker        |
| `dam`          | Delete all markers            |

---

### Text Utilities

| Command                          | Description        |
| -------------------------------- | ------------------ |
| `clear_registers`                | Reset registers    |
| `reset_h_matches`                | Reset highlighting |
| `replace`                        | Replace text       |
| `comment`                        | Toggle comment     |
| `upper` / `lower` / `capitalize` | Change case        |
| `filetype <type>`                | Set syntax mode    |

---

## Utilities

| Command                          | Description                                                       |
|----------------------------------|-------------------------------------------------------------------|
| `align`                          | Align text by a delimiter (e.g. `=`, `:`, `,`, `|`)               |
| `column_edit`                    | Insert, replace, or delete text within column ranges per line     |
| `date`                           | Copy date                                                         |
| `datetime`                       | Copy current date & time                                          |
| `file_path`                      | Copy current file path                                            |
| `json_format`                    | Format JSON in selection or entire file                           |
| `number_lines`                   | Add line numbers to each line                                     |
| `rejoin`                         | Split and rejoin selection or entire file using a delimiter       |
| `remove_empty_lines`             | Remove blank or whitespace-only lines                             |
| `reverse_lines`                  | Reverse the order of lines                                        |
| `shuffle_lines`                  | Randomly shuffle lines                                            |
| `slugify`                        | Convert text to URL-friendly slug format                          |
| `sort` / `sort_r`                | Sort selection or entire file by lines (normal or reverse)        |
| `trim` / `trim_l` / `trim_r`     | Trim whitespace (both, left, or right) per line                   |
| `unique_lines` / `dedup`         | Remove duplicate lines (keep first occurrence)                    |
| `uuid`                           | Copy random UUID                                                  |
| `wrap`                           | Wrap text to a specified width                                    |
| `json_format`                    | formats json with indentation                                     |

---

## Git Commands

| Command          | Description                           |
| ---------------- | ------------------------------------- |
| `git_status`     | Show Git status                       |
| `git_diff`       | Show diff                             |
| `git_commit_all` | Commit all                            |
| `git_blame`      | Show blame                            |
| `git_log`        | Show history                          |
| `git_refresh`    | Refresh state                         |
| `git_hunk`       | Show previous version of changed code |

---

## LSP Commands

| Command                         | Description                        |
| ------------------------------- | ---------------------------------- |
| `lsp_diagnostics`               | Show buffer diagnostics            |
| `lsp_hover`                     | Show hover info                    |
| `lsp_definition`                | Go to definition                   |
| `lsp_refresh`                   | Restart LSP for buffer             |
| `lsp_references`                | References picker                  |
| `lsp_line_diagnostics`          | Show diagnostics for current line  |
| `lsp_clear_diagnostics`         | Clear diagnostics                  |
| `lsp_clear_line_diag` / `clrld` | Clear diagnostics for current line |

---

## AI Commands

Denary automatically requests AI code completions after a short pause
in Insert mode at safe code boundaries, including before closing delimiters and
between tokens. Requests are suppressed in the middle of identifiers and in an
empty, context-free file. Suggestions appear as
dim virtual text: press `Tab` to accept the entire suggestion, or `Esc` to dismiss
it without leaving Insert mode. A second `Esc` returns to Normal mode.

| Command                          | Description                                  |
| -------------------------------- | -------------------------------------------- |
| `ai`                             | Send a custom instruction to the AI          |
| `ai_gen`                         | Manually request inline autocomplete         |
| `ai_autocomplete_toggle`         | Pause/resume autocomplete for this session   |
| `ai_autocomplete_default on|off` | Set the persisted startup default            |
| `ai_autocomplete_multifile_toggle` | Toggle open-buffer context for this session |
| `ai_autocomplete_multifile_default on|off` | Persist open-buffer context default |
| `ai_autocomplete_model`          | Choose a dedicated autocomplete model        |
| `ai_autocomplete_accept`         | Accept the visible suggestion                |
| `ai_autocomplete_dismiss`        | Dismiss the visible suggestion               |
| `ai_review`                      | Review Git diff for issues                   |
| `ai_commit_message`              | Generate a commit message from Git diff      |
| `ai_get`                         | Retrieve a non-inline AI result at the cursor |
| `ai_show`                        | Show the non-inline AI result picker         |
| `ai_remove`                      | Cancel/remove a non-inline AI request         |
| `change_ai_model`                | Change the shared AI model                    |
| `editor_info` / `info`           | Show editor state and diagnostics             |

Static configuration accepts `ai_autocomplete_enabled` (default `true`),
`ai_autocomplete_model` (falls back to `model`), and
`ai_autocomplete_delay_ms` (default `600`, clamped to 100–5000 ms). All
Ollama autocomplete requests use `ollama_url`; external autocomplete requests
use the selected OpenAI-compatible provider. Ollama remains the default.
`ai_autocomplete_multifile_enabled` defaults to `true`. Related context comes
only from loaded open buffers and uses their unsaved in-memory content. The
`ai_autocomplete_related_context_chars` setting accepts `"auto"` (the default)
or a numeric upper bound. Automatic sizing reads the selected model's Ollama
context metadata when available (or the provider's configured context window), reserves room for the current file and response, and uses
only the remaining budget for relevant buffer excerpts.

---

## HTTP Request Execution

Select a block and run:

| Command                     | Description                        |
| --------------------------- | ---------------------------------- |
| `req`                       | Make http request                  |

### Request Format

```
<HTTP_METHOD> <URL>
<Header>: <Value>
<Header>: <Value>

<Optional Body>
```

### Examples

#### GET:

```
GET https://jsonplaceholder.typicode.com/todos/1
Accept: application/json
```

#### POST:

```
POST https://jsonplaceholder.typicode.com/posts
Content-Type: application/json

{
    "title": "Hello world",
    "body": "Testing request from editor",
    "userId": 1
}
```


## Regex Replacement

Perform regex-based find and replace in the current buffer.

| Command   | Description                    |
| --------- | ------------------------------ |
| `replace` | Run a regex search and replace |

---

## Capture Groups

Parentheses create **capture groups** that can be reused in the replacement.

| Syntax | Meaning      |
| ------ | ------------ |
| `\0`   | Entire match |
| `\1`   | First group  |
| `\2`   | Second group |
| `\3`   | Third group  |

Example

Find

```
(\w+)\s+(\w+)
```

Replace

```
\2 \1
```

Result

```
hello world → world hello
```

---

## Named Capture Groups

Groups can also be named.

```
(?P<name>pattern)
```

Named groups are referenced in the replacement using:

```
\g<name>
```

Example

Find

```
(?P<first>\w+)\s+(?P<last>\w+)
```

Replace

```
\g<last> \g<first>
```

Result

```
john doe → doe john
```

---

## Case Transformations

The replacement string supports case modifiers.

| Syntax | Effect                   |
| ------ | ------------------------ |
| `\U`   | Uppercase until `\E`     |
| `\L`   | Lowercase until `\E`     |
| `\u`   | Uppercase next character |
| `\l`   | Lowercase next character |
| `\E`   | End transformation       |

Example

Find

```
select
```

Replace

```
\U\0\E
```

Result

```
select → SELECT
```

---

## Word Boundaries

`\b` matches the boundary between a word and a non-word character.

Example

Find

```
\bselect\b
```

Matches

```
select
```

Does not match

```
selected
```

---

## Example: SQL Keyword Formatting

Find

```
\b(select|from|where|join|group\s+by|order\s+by)\b
```

Replace

```
\U\0\E
```

Result

```
select name from users where id = 1
↓
SELECT name FROM users WHERE id = 1
```

## Configuration

Denary loads its configuration from a platform-specific directory.

### Config Location

#### Linux

```
~/.config/denary/init.py
```

#### macOS

```
~/Library/Application Support/denary/init.py
```

#### Windows (WSL recommended)

```
~/.config/denary/init.py
```

If the file does not exist, create it manually.

Example:

```bash
mkdir -p ~/.config/denary
nano ~/.config/denary/init.py
```

### AI Static Configuration

AI settings are stored separately in `static_config.json` in the same Denary
configuration directory. A complete example is available at
[`examples/static_config.json`](examples/static_config.json). Copy its contents
to your user `static_config.json`, then change the provider URL, model names,
and environment-variable name for your setup.

For example, on Linux:

```bash
cp examples/static_config.json ~/.config/denary/static_config.json
export CLOUD_API_KEY="your-secret-key"
```

The example keeps inline autocomplete on Ollama while routing chat and other
AI commands to an external OpenAI-compatible provider. To use remote Ollama,
change `ollama_url`, for example:

```json
{"ollama_url": "http://my-ollama-server:11434"}
```

`api_key_env` must contain only the environment variable name, such as
`CLOUD_API_KEY`; never put the secret itself in this JSON file.

---

### LSP Configuration

The `init.py` file allows you to:

* Configure Language Servers (LSPs)
* Map file extensions to language identifiers
* Provide custom initialization settings

Denary expects two dictionaries:

```python
LSP_CONFIG = {}
EXT_TO_LANG = {}
```

---

### Example Configuration

#### LSP_CONFIG

```python
LSP_CONFIG = {
    "python": {
        "cmd": [
            "/home/user/.config/denary/pylsp_venv/bin/python",
            "-m",
            "pylsp",
        ],
        "settings": {
            "pylsp": {
                "plugins": {
                    "flake8": {"enabled": True, "maxLineLength": 100},
                    "pyflakes": {"enabled": False},
                    "pycodestyle": {"enabled": False},
                }
            },
        },
    },

    "javascript": {
        "cmd": [
            "/home/user/.config/denary/ts_server/node_modules/.bin/typescript-language-server",
            "--stdio",
        ],
    },

    "c": {
        "cmd": [
            "/home/user/.config/denary/clangd_venv/bin/clangd",
            "--all-scopes-completion",
            "--clang-tidy",
            "--offset-encoding=utf-8",
        ],
    },

    "csharp": {
        "cmd": [
            "mono",
            "/home/user/.config/denary/omnisharp/OmniSharp.exe",
            "--languageserver",
        ],
    },

    "sql": {
        "name": "sqls",
        "cmd": [
            "/home/user/go/bin/sqls",
        ],
    },
}
```

`name` is optional and defaults to the language-server executable name. It gives
server-specific command workflows, such as SQLS connection switching, a stable
identity.

---

### EXT_TO_LANG

This maps file extensions to language keys defined in `LSP_CONFIG`.

```python
EXT_TO_LANG = {
    ".py": "python",
    ".c": "c",
    ".h": "c",
    ".cpp": "c",
    ".js": "javascript",
    ".ts": "javascript",
    ".jsx": "javascript",
    ".tsx": "javascript",
    ".cs": "csharp",
    ".sql": "sql",
}
```

---

## How It Works

1. You open a file:

   ```
   denary main.py
   ```

2. Denary checks the file extension (`.py`)

3. It maps it using `EXT_TO_LANG`

4. It loads the corresponding LSP from `LSP_CONFIG`

5. The language server is started using the `cmd` array

---

## Recommended Structure for LSP Tools

It is recommended to keep all LSP-related tools inside:

```
~/.config/denary/
```

Example layout:

```
~/.config/denary/
├── init.py
├── pylsp_venv/
├── clangd_venv/
├── ts_server/
└── omnisharp/
```

This keeps your setup portable and isolated.

---

## Important Notes

* `cmd` must be a list (not a string)
* Paths must be absolute
* The language key in `EXT_TO_LANG` must exist in `LSP_CONFIG`
* If an extension is not mapped, no LSP will be started

