Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "writui"
version = "0.18.0"
version = "0.19.0"
edition = "2024"
description = "A terminal writing app. No nonsense. Markdown. Local. Encrypted."
license = "MIT"
Expand Down
38 changes: 12 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ A writing app for the terminal. No nonsense. Markdown. Local. Encrypted.
- **Plain markdown.** You write markdown. Each post starts with a `# Title`.
It stays plain markdown on screen, formatted as you type: headings and
bold in bold, italics in italics, code in green, with the markdown
symbols faded.
symbols faded. (Settings can tone that down to faded symbols, italics
and green code, or turn it off.) Lists carry on as you press Enter, and Tab nests an
item.
- **Mouse and keyboard.** Click, scroll and press buttons with the mouse, or
use shortcuts. The bar at the bottom of the screen shows the shortcuts
for where you are, and you can click those too. While writing, Ctrl+K
Expand Down Expand Up @@ -197,39 +199,23 @@ can open it after a fingerprint, is kept in `~/.config/writui/touch-id/`.
for 30 days.
- **Settings** (`s` on the list): your name, added as the
`author` in exported front matter, how wide the text is while
writing (68 characters unless you change it), and whether your
writing (68 characters unless you change it), how much markdown
styling you see (full, minimal or off), and whether your
terminal's font is a [Nerd Font](https://www.nerdfonts.com), so writui
can use its icons.
can use its icons. Under each setting is a line saying what it does now.

### Cmd keys on a Mac

In the editor, Cmd works like Ctrl (Cmd+Z undoes, Cmd+X cuts, Cmd+A
selects all, Cmd+K opens the menu) wherever your terminal passes Cmd on to
programs. Cmd+V is your terminal's paste, which writui understands.

Cmd+C is different: terminals keep it to copy *their own* selection, and
writui draws its own selection, so there's nothing for the terminal to
copy. Ctrl+C always works. To make Cmd+C copy in writui too, tell your
terminal to pass it on:

- **Alacritty**, in `~/.config/alacritty/alacritty.toml`:

```toml
[[keyboard.bindings]]
key = "C"
mods = "Command"
action = "ReceiveChar"
```

- **Ghostty**, in its config (it still copies the terminal's own selection
when there is one):

```
keybind = performable:super+c=copy_to_clipboard
```

- **iTerm2** (not tried yet): Settings → Keys → Key Bindings, add Cmd+C
with the action "Send Escape Sequence" and `[99;9u`.
Cmd+C works too, in any terminal, with nothing to set up: whatever you
select in writui goes on the clipboard right away, so it's there when you
press Cmd+C (which the terminal keeps for itself). It doesn't get in the
way of what you'd copied before: typing or pasting over a selection puts
that back on the clipboard, and Cmd+V over a selection pastes it, not
the selection. Ctrl+C copies too.

Cmd+K needs nothing in Alacritty (it sends Ctrl+L, which also opens the
menu). In Ghostty and iTerm2 it clears the screen unless you pass it on
Expand Down
59 changes: 59 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,62 @@ password unlock quietly makes a new one.
anyway), so Touch ID can be turned on from Settings without retyping the
password. Opening with the key skips Argon2, so Touch ID unlocks are
instant. Changing the password, when it lands, must redo the Touch ID file.

## Markdown styling: full, minimal, off

A three-way setting replaced the "bold title" switch proposed in PR #31
(closed), which the creator didn't like as a one-off. The styling is still
worked out in full by the CommonMark parser; minimal and off only map the
styles at the end (minimal keeps the faded symbols, italics and green
code; off keeps nothing), so links are found, and open, the same in every mode. Stored in
the vault's key/value table as `markdown_styling`, so no migration: a
vault without it reads as `full`.

## Settings say what each value does

Each setting takes two rows (name and value, then a faded line saying what
the current value does) and a faded rule between settings. The screen is
drawn by hand rather than with ratatui's `List`, so clicks on the rules do
nothing and each option can be clicked. A reversed block for the selected
setting was too loud, so a bar (`┃`, box drawing, so any font has it) sits
left of the column instead (inside it, moving the settings over, if the
terminal is too narrow). Settings with a few options show them all
(`on | off`), the current one reversed with a space either side and the
rest faded; ←/→ and clicks pick one, Enter goes to the next.

## Lists carry on: from the line's own prefix

Enter, Tab and Shift+Tab look only at the current line's prefix (any `>`
quote markers, indent, `-`/`*`/`+` or `1.`/`1)`, an optional task box),
like the hanging indent does, not the markdown parser: it's what the
writer sees, and it works on half-typed lists. Nesting lines an item up
with the text of the item above at its level (2 columns after `- `, 3
after `1. `), which is what CommonMark needs to make it a child; outdenting
goes back to the nearest item above with a smaller indent. It only looks
back within the paragraph (to the last blank line). A nested numbered item
is renumbered 1: CommonMark only lets a numbered list starting at 1
interrupt a paragraph, so `3.` nested under an item is just text. Brought
out, it takes the number after the item above at its new level. Nothing
else is renumbered. Enter on an empty item ends the list (or outdents a
nested one), as in most editors. Shift+Enter arrives as Shift+Enter with
the kitty protocol, and as Alt+Enter where the terminal sends ESC CR (the
creator's Alacritty config does), so both are a plain new line.

## Cmd+C: the selection goes on the clipboard as it's made

Terminals keep Cmd+C to copy their own selection, and writui (which draws
its own) never sees it, so Cmd+C couldn't work without terminal config.
Mouse capture can't be given up (everything is clickable), and there's no
escape sequence to set the terminal's selection. So selecting text in the
editor puts it on the system clipboard straight away (after a drag ends,
not on every mouse move). To keep that from clobbering what was copied
before, it's provisional: writui remembers the clipboard's text from
before the selection. Typing, deleting or pasting over the selection puts
that back; a paste over the selection whose text is the selection itself
(the terminal's Cmd+V reading the clipboard) pastes the earlier text
instead. Leaving the selection (click, arrow, Esc) keeps it on the
clipboard, as Cmd+C would. Nothing is put back if something else was
copied in the meantime. The trade-off: "select, Cmd+C, then type over the
selection" loses the copy (Cmd+X is the way to do that). Non-text clipboard
contents (an image) can't be remembered, so selecting text replaces them.
Tests use a per-thread fake clipboard rather than the real one.
8 changes: 7 additions & 1 deletion docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ on the password screens, boxed password fields, Esc to clear or quit, and
the whole repo run through `cargo fmt` (checked in CI). `0.17.0` narrows
the logo, adds an Unlock button, trims the setup screen, and makes Esc on
the list quit without asking. `0.18.0` unlocks with Touch ID on Macs
(asked for out of order). Next:
(asked for out of order), and `0.19.0` adds the Markdown styling setting,
list continuation and Cmd+C (also out of order; it replaces the closed
PR #31's bold-title switch). Next:

1. **Try online** (writui.com/try), below: the creator wants it next, and
it must work on a phone. Probably two or three PRs, in the order listed.
Expand Down Expand Up @@ -190,6 +192,10 @@ The creator's batch of ideas, after M3.
the right-click menu
- [x] Tab / Shift+Tab cycle the sort; Ctrl/Cmd+Up/Down to the start / end
of the post; Ctrl/Cmd+Left/Right on by sentence or row
- [x] Markdown styling setting (full / minimal / off); each setting says
what it does, with lines between them; lists carry on with Enter,
Tab / Shift+Tab nest them; Cmd+C copies with no terminal setup
(`0.19.0`, asked for out of order)

## Later

Expand Down
91 changes: 82 additions & 9 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md).
paste, so it just works. Cmd+C is the terminal's copy of *its own*
selection; in nano that's what you see, because nano leaves the mouse
to the terminal, but writui draws its own selection (to know what's
selected), so the terminal has nothing to copy. The README shows the
one line of terminal config that passes Cmd+C on to writui. Alacritty
selected), so the terminal has nothing to copy of its own. So writui
puts whatever is selected on the clipboard as soon as it's selected
(see Editing), and Cmd+C works with no setup. Passing Cmd+C on to
writui (the README's terminal config) still works too. Alacritty
sends Ctrl+L for Cmd+K, so Ctrl+L opens (and closes) the menu too, and
Cmd+K works there without any config.

Expand Down Expand Up @@ -453,7 +455,31 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md).
- Home/End go to the start/end of the row on screen, and so do
Ctrl+Left/Right (and Cmd+Left/Right on macOS); Ctrl+Home/Ctrl+End
to the start/end of the post. PageUp/PageDown move a screenful.
- Tab types two spaces (to be configurable in settings).
- Tab types two spaces (to be configurable in settings), except at the
start of a list item (below).

> in the editor itself, i want automatic continuation for bullet points,
> checkboxes, numbered lists etc. on pressing enter. i also want tab and
> shift+tab to do the right thing at the start of a list line i.e. indent
> or unindent the item. it doesn't have to be perfect covering every
> case, just good enough for the common cases

- Lists carry on: Enter in a list item starts the next one with the same
bullet (`-`, `*`, `+`), the next number (`1.` → `2.`, `1)` → `2)`), and
an unticked `[ ] ` if it's a task, at the same indent. Quotes (`> `)
carry on the same way. Enter mid-item splits it; with the cursor before
the bullet, it's a plain new line.
- Enter on an empty item ends the list: the bullet goes, leaving a blank
line to type on. A nested empty item comes out a level instead.
- Shift+Enter (or Option+Enter) is always a plain new line.
- With the cursor at the start of an item (anywhere up to the end of its
bullet, number or task box), Tab nests it under the item above, lined
up with that item's text; Shift+Tab brings it back out to its parent's
level. Elsewhere Tab types two spaces, and Shift+Tab does nothing.
- A numbered item nested this way starts again at 1 (markdown only
takes a nested numbered list that starts at 1 as a list); brought back
out, it carries on from the number above it. Other numbers aren't
redone.
- Opening a post puts the cursor back where it was when the post was last
closed, as near the middle of the screen as the text allows. A post that
has never been opened starts with the cursor at the end.
Expand All @@ -474,6 +500,21 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md).
If there's no system clipboard (e.g. no display), they still work inside
writui. Pasting from the terminal (Cmd+V) also works.

> finally, please make Cmd+c and Cmd+v work on mac for copy and paste. i
> mean, if it works on nano, why doesn't it work here?

- Selecting text puts it on the clipboard straight away (once a drag ends),
so Cmd+C needs no terminal setup: the terminal's own Cmd+C finds nothing
of its own to copy, and the selection is already there. Until it's
copied for real (Ctrl+C, Cut, the menu), that copy is provisional:
- Typing, Backspace or Delete over the selection, or pasting over it,
puts back what was on the clipboard before it was selected (unless
something else was copied since).
- Cmd+V (or Ctrl+V) over a selection pastes what was on the clipboard
before it, not the selection itself.
- Moving on from the selection (a click, an arrow key, Esc) leaves it on
the clipboard, as if copied.

> add a context menu for when i right click on a selection to cut, copy,
> paste (disabled if clipboard is empty)

Expand Down Expand Up @@ -574,6 +615,19 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md).
the text, not the bullet. On screen only; no spaces are added to the text.
- Later: syntax highlighting in code blocks, per language.

> add a new setting called markdown styling with values "off", "minimal"
> (no bold text, just markdown markers are muted), and "full" (default,
> current)

> in the minimal setting, i stil want green for code

> keep italic in minimal mode

- All of that is the **full** styling, the default. Settings can make it
**minimal** (the markdown symbols faded, italics italic and code green;
no bold or underlines) or turn it **off** (everything plain). Links open the same either way. The history shows versions in
the same styling.

## Web preview

> A lot of the writing I'm doing is actually for work etc. so I want to be
Expand Down Expand Up @@ -742,21 +796,40 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md).

**Details**

- A Settings screen, reached with `s` on the list (it's on the hint bar). Each row is a setting and its value; Enter or a
click changes it in a dialog; Esc goes back to the list (its hint
just says "back"). Changes are
stored right away.
> for each setting add a line below explaning what the currnet behavior
> is in lighter text

> add a separator line between settings

> also, in the settings screen, there is too much color. maybe just keep
> a bar on the left to indicate which one wer'e one. also i want the
> selection to appear as on | off with the current selection
> highlighted, this way all options are shown

- A Settings screen, reached with `s` on the list (it's on the hint bar).
Each setting shows its name and value, and below that, faded, a line on
what the current value does (it changes with the value); a faded line
separates one setting from the next. The selected setting has a bar
(`┃`) to its left; nothing else is highlighted. Settings with a few
options show them all on the right, like `on | off` or
`full | minimal | off`, the current one highlighted (reversed) and the
others faded. ←/→ or a click on an option picks it; Enter or a click
elsewhere on the setting goes to the next option. Others (Author, Line
width) show their value, and Enter or a click changes it in a dialog.
Esc goes back to the list (its hint just says "back"). Changes are stored right away. A short screen
scrolls to keep the selected setting in view.
- For now:
- **Author**: suggested as `author` in the front matter of exports
(after `title`), unless it's empty. Like the description and slug, it
follows Settings on later exports unless it was changed by hand in the
front matter.
- **Line width**: how wide the text is in the editor (and the history),
from 40 to 120 characters; 68 by default.
- **Markdown styling**: full (the default), minimal or off (see Live
markdown formatting), shown as `full | minimal | off`.
- **Nerd Font**: on or off (off by default), for terminals whose font is
a Nerd Font: writui uses its icons (so far, the search field's
magnifier). Enter or a click switches it, no dialog; the hint says
"Enter switch".
magnifier). Shown as `on | off`.
- Later: what Tab types, default preview preset, API keys, default
model, auto-lock timeout — all stored in the vault.
- The vault file location has to be known *before* unlocking, so it lives
Expand Down
6 changes: 4 additions & 2 deletions site/demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ Type "I write every morning, and lately I do it in a **terminal**. "
Type "It's *quiet*: one column of text, nothing else."
Enter
Enter
# Enter carries the list on; on an empty item, it ends it.
Type "- Plain `markdown`, formatted as I type"
Enter
Type "- Encrypted with my password"
Type "Encrypted with my password"
Enter
Type "Versions whenever I want them"
Enter
Type "- Versions whenever I want them"
Enter
Enter
Type "> The best tool is the one that gets out of the way."
Expand Down
Binary file modified site/static/demo.mp4
Binary file not shown.
Binary file modified site/static/demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions src/tui/editor/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::ops::Range;
use ropey::Rope;
use unicode_segmentation::UnicodeSegmentation;

use super::lists;
use crate::vault::{TITLE_PREFIX, with_title_prefix};

/// The first position the cursor can be at: just after `# `.
Expand Down Expand Up @@ -141,6 +142,62 @@ impl Buffer {
}
}

/// Enter: a new line, which carries on the list item or quote the
/// cursor is in. On an empty item, it ends the list instead (or, if it's
/// nested, brings it a level out).
pub fn newline(&mut self) {
let line = self.rope.char_to_line(self.cursor);
let start = self.rope.line_to_char(line);
let text = self.line_text(line);
let prefix = lists::prefix(&text).filter(|p| self.cursor >= start + p.len);
match prefix {
Some(prefix) if line > 0 && self.selection().is_none() => {
if !prefix.empty(&text) {
self.insert(&format!("\n{}", prefix.next(&text)));
} else if !self.reindent(line, false) {
self.rope.remove(start..start + text.chars().count());
self.cursor = start;
}
}
_ => self.insert("\n"),
}
}

/// Tab or Shift+Tab with the cursor at the start of a list item (in its
/// bullet, number or task box, or before them): nest the item under the
/// one above, or bring it a level out. False if the cursor isn't there.
pub fn indent_item(&mut self, deeper: bool) -> bool {
let line = self.rope.char_to_line(self.cursor);
let start = self.rope.line_to_char(line);
let at_start = lists::prefix(&self.line_text(line))
.is_some_and(|p| p.is_item() && self.cursor <= start + p.len);
line > 0 && self.selection().is_none() && at_start && self.reindent(line, deeper)
}

/// Nest the list item on `line` deeper, or bring it out a level.
fn reindent(&mut self, line: usize, deeper: bool) -> bool {
// The lines of its paragraph, up to it.
let mut first = line;
while first > 1 && !self.line_text(first - 1).trim().is_empty() {
first -= 1;
}
let lines: Vec<String> = (first..=line).map(|l| self.line_text(l)).collect();
let Some((range, indent)) = lists::reindent(&lines, line - first, deeper) else {
return false;
};
let start = self.rope.line_to_char(line);
let (from, to) = (start + range.start, start + range.end);
self.rope.remove(from..to);
self.rope.insert(from, &indent);
let new_to = from + indent.chars().count();
if self.cursor >= to {
self.cursor = self.cursor - to + new_to;
} else if self.cursor > from {
self.cursor = new_to.min(self.cursor);
}
true
}

/// Delete the selection, or the grapheme before the cursor (joining
/// lines at a line start).
pub fn backspace(&mut self) {
Expand Down
Loading
Loading