From 0607e8f3073ca2e66083396d220a59ceab0664d8 Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:49:03 +0700 Subject: [PATCH 1/7] A narrower logo, with room under it The "writui" letters on the password screens are narrower (42 columns instead of 54), and there's an extra blank line between the logo and what's under it. Co-Authored-By: Claude Opus 5.5 --- src/tui/widgets.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/tui/widgets.rs b/src/tui/widgets.rs index d6ff8de..fbbae48 100644 --- a/src/tui/widgets.rs +++ b/src/tui/widgets.rs @@ -43,24 +43,27 @@ pub fn overview() -> Vec> { /// into those three parts. #[rustfmt::skip] const BANNER: [[&str; 3]; 6] = [ - [" ", " ", " ██ ██"], - ["▄▄▄ ", "▄▄", " ██"], - [" ▀██▄ ", "██", " ██ ██ ██ ▄████ █████ ███████ ██ ██ █████"], - [" ▀██▄ ", "██", " ██ ▄▄ ██ ███▀ ██ ██ ██ ██ ██"], - [" ▄██▀ ", "██", " ██▄██▄██ ██ ██ ██ ██ ██ ██"], - ["▄██▀ ", "██", " ▀█▀ ▀█▀ ██ ████████ ▀█████ ▀██▄▄▄██ ████████"], + [" ", " ", " ██ ██"], + ["▄▄▄ ", "▄▄", " ██"], + [" ▀██▄ ", "██", " ██ ██ ██ ▄██ ▀██ █████ ██ ██ ▀██"], + [" ▀██▄ ", "██", " ██ ▄ ██ ███▀ ██ ██ ██ ██ ██"], + [" ▄██▀ ", "██", " ██▄█▄██ ██ ██ ██ ██ ██ ██"], + ["▄██▀ ", "██", " ▀█▀ ▀█▀ ██ ▄██▄ ▀███ ▀████▀ ▄██▄"], ]; -/// The password screens' title: the banner, or just "writui" when `area` -/// is too small to fit the banner above `other_lines` more lines. +/// The password screens' title: the banner and a blank line under it, or +/// just "writui" when `area` is too small to fit those above `other_lines` +/// more lines. pub fn title(area: Rect, other_lines: usize) -> Vec> { - if area.width < COLUMN_WIDTH || (area.height as usize) < BANNER.len() + other_lines { + if area.width < COLUMN_WIDTH || (area.height as usize) < BANNER.len() + 1 + other_lines { return vec![Line::from("writui".bold())]; } - BANNER + let mut lines: Vec = BANNER .iter() .map(|[mark, bar, name]| Line::from(vec![Span::raw(*mark), bar.green(), Span::raw(*name)])) - .collect() + .collect(); + lines.push(Line::default()); + lines } /// A column of at most `width`, horizontally centred in `area`. @@ -180,7 +183,7 @@ mod tests { #[test] fn the_banner_fits_the_column() { let lines = title(Rect::new(0, 0, 100, 30), 10); - assert_eq!(lines.len(), BANNER.len()); + assert_eq!(lines.len(), BANNER.len() + 1); assert!(lines.iter().all(|line| line.width() <= COLUMN_WIDTH as usize)); } From b952addf77033c1a231872b266e5a0919ddc9ac1 Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:49:03 +0700 Subject: [PATCH 2/7] An Unlock button next to the password The unlock screen has a clickable Unlock button right of the password box. It's dimmed until something's typed, and left out in windows too narrow for it; Enter still unlocks as before. Co-Authored-By: Claude Opus 5.5 --- src/tui/unlock.rs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/tui/unlock.rs b/src/tui/unlock.rs index 642f6e0..8ef118c 100644 --- a/src/tui/unlock.rs +++ b/src/tui/unlock.rs @@ -2,10 +2,10 @@ use ratatui::Frame; use ratatui::crossterm::event::{Event, KeyCode, MouseButton, MouseEventKind}; -use ratatui::layout::{Constraint, Layout, Rect}; -use ratatui::style::Stylize; +use ratatui::layout::{Constraint, Layout, Position, Rect}; +use ratatui::style::{Style, Stylize}; use ratatui::text::Line; -use ratatui::widgets::Paragraph; +use ratatui::widgets::{Block, Paragraph}; use super::Action; use super::hints::{HintBar, hint}; @@ -27,6 +27,8 @@ pub struct Unlock { locked: bool, error: Option<&'static str>, busy: bool, + /// Where the Unlock button was drawn, for clicks. + button: Rect, hints: HintBar, } @@ -84,6 +86,27 @@ impl Unlock { frame.set_cursor_position(cursor); } + // The Unlock button, right of the password box and as tall as it, + // left out if the window is too narrow for all of it. Dimmed while + // there's no password for it to try. + let button = Rect::new(field_area.right() + 2, field_area.y, 12, height); + self.button = if area.contains(Position::new(button.right() - 1, button.bottom() - 1)) { + button + } else { + Rect::default() + }; + if !self.button.is_empty() { + let style = if self.field.is_empty() || self.busy { + Style::new().dim() + } else { + Style::new().bold() + }; + let block = Block::bordered().border_style(style); + let label = Paragraph::new(Line::from("Unlock").style(style)).centered(); + frame.render_widget(label, block.inner(button)); + frame.render_widget(block, button); + } + // Esc clears the password, or quits if there's nothing to clear. let mut hints = vec![hint("Enter", "unlock", Cmd::Unlock)]; if self.field.is_empty() { @@ -114,6 +137,9 @@ impl Unlock { Action::None } Event::Mouse(mouse) if mouse.kind == MouseEventKind::Down(MouseButton::Left) => { + if self.button.contains(Position::new(mouse.column, mouse.row)) { + return self.run(Cmd::Unlock); + } match self.hints.hit(mouse.column, mouse.row) { Some(cmd) => self.run(cmd), None => Action::None, From 0b330072cd1101fde5ae2be69c658def0d13c06b Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:49:03 +0700 Subject: [PATCH 3/7] A shorter Create your vault screen The paragraph about the password being unrecoverable is gone: the Overview below says the same. Co-Authored-By: Claude Opus 5.5 --- src/tui/setup.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/tui/setup.rs b/src/tui/setup.rs index bb6b112..62715ff 100644 --- a/src/tui/setup.rs +++ b/src/tui/setup.rs @@ -70,10 +70,6 @@ impl Setup { Line::default(), Line::from("Create your vault"), Line::default(), - Line::from("Everything you write in writui is encrypted with this password."), - Line::from("There is no way to recover it: if you forget the password, your"), - Line::from("writing is gone for good."), - Line::default(), // The two password boxes go here, side by side. Line::default(), Line::default(), @@ -88,7 +84,7 @@ impl Setup { rest.extend([Line::default(), Line::from(note.dim())]); } let mut lines = title(body, rest.len()); - let first_field_line = lines.len() as u16 + 7; + let first_field_line = lines.len() as u16 + 3; lines.extend(rest); let area = centered(body, COLUMN_WIDTH, lines.len() as u16); From 93cc32d32004244e2cf8aa0f06278b37fd9c5140 Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:49:03 +0700 Subject: [PATCH 4/7] Esc on the list quits without asking Everything is saved as you type, so there's nothing to confirm. In a search, Esc still empties it first. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- src/tui/list.rs | 28 ++++------------------------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 9759b7f..1168473 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ the password each time it starts. - **Posts** are listed with the most recently changed first. Open one with Enter or a click, start a new one with `n` (or Enter: "New" is highlighted when the list opens). The list's shortcuts are single keys, - shown next to "New" and "Import" and at the bottom. Esc asks before quitting. writui opens + shown next to "New" and "Import" and at the bottom. Esc quits. writui opens where you left it: on the post you were writing, cursor and all. - **Search:** press `/` (or click "Type to filter (/)") and type. Posts with every word you type, anywhere in their text or front matter, are listed, diff --git a/src/tui/list.rs b/src/tui/list.rs index 823e5af..f0daa21 100644 --- a/src/tui/list.rs +++ b/src/tui/list.rs @@ -130,9 +130,7 @@ enum Cmd { UpdateImported, ImportAsNew, Delete, - /// Esc on the list: asks first. Quit, - ConfirmQuit, Back, ConfirmDelete, CancelDialog, @@ -838,21 +836,7 @@ impl List { fn run(&mut self, cmd: Cmd) -> Action { match cmd { - Cmd::Quit => { - let dialog = Dialog::new( - "Quit", - vec![Line::from("Quit writui?"), Line::from("Your writing is saved.".dim())], - vec![ - Button { label: "Quit", key: "y", cmd: Cmd::ConfirmQuit, danger: false }, - Button { label: "Cancel", key: "n", cmd: Cmd::CancelDialog, danger: false }, - ], - 0, - 1, - ); - self.dialog = Some((0, dialog)); - Action::None - } - Cmd::ConfirmQuit => Action::Quit, + Cmd::Quit => Action::Quit, Cmd::Lock => Action::Lock, Cmd::New if !self.trash_view() => Action::NewPost, Cmd::Settings if !self.trash_view() => Action::ShowSettings, @@ -1189,7 +1173,7 @@ mod tests { } #[test] - fn single_keys_and_esc_asks_before_quitting() { + fn single_keys_and_esc_quits() { let posts = vec![post(1, "One"), post(2, "Two")]; let mut list = List::new(Mode::Posts { deleted_count: 0 }, posts, None); let text = screen(&mut list); @@ -1215,13 +1199,9 @@ mod tests { press(&mut list, KeyCode::Char('d'), KeyModifiers::NONE); assert!(screen(&mut list).contains("Move “One” to the Trash?")); press(&mut list, KeyCode::Esc, KeyModifiers::NONE); - // Esc: asks; n keeps going, y quits. - assert!(matches!(press(&mut list, KeyCode::Esc, KeyModifiers::NONE), Action::None)); - assert!(screen(&mut list).contains("Quit writui?")); - assert!(matches!(press(&mut list, KeyCode::Char('n'), KeyModifiers::NONE), Action::None)); assert!(list.dialog.is_none()); - press(&mut list, KeyCode::Esc, KeyModifiers::NONE); - assert!(matches!(press(&mut list, KeyCode::Char('y'), KeyModifiers::NONE), Action::Quit)); + // Esc quits straight away: the writing is already saved. + assert!(matches!(press(&mut list, KeyCode::Esc, KeyModifiers::NONE), Action::Quit)); } #[test] From def13451eaebf936683f7495efe9ceb412335260 Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:49:03 +0700 Subject: [PATCH 5/7] Version 0.17.0, and the spec and plan for it Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 2 +- docs/PLAN.md | 4 +++- docs/SPEC.md | 30 ++++++++++++++++++++++++++---- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2e599d..77e4a0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2917,7 +2917,7 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "writui" -version = "0.16.0" +version = "0.17.0" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 9a4a449..5e05428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "writui" -version = "0.16.0" +version = "0.17.0" edition = "2024" description = "A terminal writing app. No nonsense. Markdown. Local. Encrypted." license = "MIT" diff --git a/docs/PLAN.md b/docs/PLAN.md index 988d15e..0376bcf 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -21,7 +21,9 @@ The creator's batch of list / editor ideas shipped in `0.12.0`, and the `writui info` and Select All in `0.14.0`, Tab cycling the sort and Ctrl/Cmd+arrow jumps in `0.15.0`, and in `0.16.0` the logo as a big banner on the password screens, boxed password fields, Esc to clear or quit, and -the whole repo run through `cargo fmt` (checked in CI). Next: +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. 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. diff --git a/docs/SPEC.md b/docs/SPEC.md index 9f7800a..e3d48e3 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -170,6 +170,12 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md). column. Windows narrower than that, or too short to fit it, get the plain "writui" title instead. + > can you try making "writui" less wide? also, i think some space is + > needed between the logo and the password box. think of the aesthetics + + The letters are narrow (the name is 42 columns, not 54), and a blank line + under the logo leaves two rows before what follows. + > put a box around the password > and esc to clear password, and if clear esc to quit @@ -178,6 +184,12 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md). vault" puts its two side by side, so the banner still fits 30 rows). Esc clears what's typed; with nothing typed, Esc quits. + > and to the right of the password box add a clickable "Unlock" button + +- The unlock screen has an "Unlock" button right of the password box, as + tall as it, dimmed until something's typed. A window too narrow for it + leaves it out (Enter still unlocks). + > and finally, on the password screen, let's add an "Overview" describing > how the app works in no more than 5-6 points, each no longer than one > line. this is for new users and returning ones, run the copy by me once @@ -190,6 +202,11 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md). - Posts save as you type; Ctrl+S also keeps a version to go back to. - While writing, Ctrl+K shows every command and its shortcut. - Ctrl+O locks writui; unlocking takes you back where you were. + + > you know, with the overview in palce, we dont need the initla para + + So "Create your vault" no longer has its own paragraph about the + password being unrecoverable: the Overview's first two points say it. - Change password — planned. ## The screens @@ -245,9 +262,14 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md). > can you add j/k support for down and up on the list page (while i'm > not typing in the filter box)? - > no need to mention them in the bottom menu Ctrl+N and Ctrl+D still work but aren't shown. Esc asks "Quit - writui?" (Quit is the selected button, `y`); Ctrl+Q and Ctrl+C still quit - straight away, unshown. + > no need to mention them in the bottom menu Ctrl+N and Ctrl+D still work but aren't shown. Esc quits; Ctrl+Q + and Ctrl+C also quit, unshown. + + > also remove the quit confirmation on th elist screen on pressing esc, i + > think it's not necessary + + (Esc used to ask "Quit writui?" first. Everything is saved as you type, + so there's nothing to lose.) > add a line gap above type to filter. and below type to filter, i want > "Sort by": "Updated", "Created", "Title". for filter use the maginifier @@ -282,7 +304,7 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md). matches, Delete deletes it. Esc empties the field, then leaves it (to the first post). The empty field is just a row ↑/↓ pass through; a click on a post leaves the field too. On the posts, Esc empties the - search before it asks to quit. + search before it quits. > let's put "Sort by: Updated " on the right of the "type to filter" > (within the main viewport), and make it a dropdown that i can click and From c0540d11c9456c2cf70f1df3d0c0f66e588779b3 Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:50:40 +0700 Subject: [PATCH 6/7] Line up the dots of the i's in the logo They sat a little left of their stems. Co-Authored-By: Claude Opus 5.5 --- src/tui/widgets.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/tui/widgets.rs b/src/tui/widgets.rs index fbbae48..204daac 100644 --- a/src/tui/widgets.rs +++ b/src/tui/widgets.rs @@ -43,7 +43,7 @@ pub fn overview() -> Vec> { /// into those three parts. #[rustfmt::skip] const BANNER: [[&str; 3]; 6] = [ - [" ", " ", " ██ ██"], + [" ", " ", " ██ ██"], ["▄▄▄ ", "▄▄", " ██"], [" ▀██▄ ", "██", " ██ ██ ██ ▄██ ▀██ █████ ██ ██ ▀██"], [" ▀██▄ ", "██", " ██ ▄ ██ ███▀ ██ ██ ██ ██ ██"], @@ -187,6 +187,19 @@ mod tests { assert!(lines.iter().all(|line| line.width() <= COLUMN_WIDTH as usize)); } + #[test] + fn the_dots_of_the_is_sit_over_their_stems() { + let columns = |line: &str, piece: &str| -> Vec { + let chars: Vec = line.chars().collect(); + let piece: Vec = piece.chars().collect(); + (0..chars.len()).filter(|&i| chars[i..].starts_with(&piece)).collect() + }; + let dots = columns(BANNER[0][2], "██"); + let stems: Vec = + columns(BANNER[2][2], "▀██").into_iter().filter(|&i| i > 10).map(|i| i + 1).collect(); + assert_eq!(dots, stems); + } + #[test] fn small_windows_get_the_plain_title() { assert_eq!(title(Rect::new(0, 0, 60, 30), 10).len(), 1); From f6907a00cc1e6cca2d83cc59ee75950c26b73612 Mon Sep 17 00:00:00 2001 From: Aakash N S <1560745+aakashns@users.noreply.github.com> Date: Tue, 13 Oct 2026 00:51:42 +0700 Subject: [PATCH 7/7] "Write your posts in markdown" in the Overview Co-Authored-By: Claude Opus 5.5 --- docs/SPEC.md | 2 +- src/tui/widgets.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SPEC.md b/docs/SPEC.md index e3d48e3..84a2aed 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -198,7 +198,7 @@ in [DECISIONS.md](DECISIONS.md); the build order lives in [PLAN.md](PLAN.md). "Overview" (creator-approved copy): - All your writing lives in one file, encrypted with this password. - Forget the password and the writing is gone: there's no recovery. - - Write in markdown. Every post starts with a # title. + - Write your posts in markdown. Every post starts with a # title. - Posts save as you type; Ctrl+S also keeps a version to go back to. - While writing, Ctrl+K shows every command and its shortcut. - Ctrl+O locks writui; unlocking takes you back where you were. diff --git a/src/tui/widgets.rs b/src/tui/widgets.rs index 204daac..bf2331f 100644 --- a/src/tui/widgets.rs +++ b/src/tui/widgets.rs @@ -28,7 +28,7 @@ pub fn overview() -> Vec> { const POINTS: [&str; 6] = [ "All your writing lives in one file, encrypted with this password.", "Forget the password and the writing is gone: there's no recovery.", - "Write in markdown. Every post starts with a # title.", + "Write your posts in markdown. Every post starts with a # title.", "Posts save as you type; Ctrl+S also keeps a version to go back to.", "While writing, Ctrl+K shows every command and its shortcut.", "Ctrl+O locks writui; unlocking takes you back where you were.",