diff --git a/DESIGN.md b/DESIGN.md index 263acbd..56b1322 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -197,6 +197,58 @@ lines. beside the node. `:marks`, `:jumps` and `:registers` list them; Enter goes to the mark or the place, or pastes the register. +**Connect mode and portals** (settled in part 8d) +- `C` starts connect mode on the cursor's node: the cursor goes to another + node as usual (`hjkl`, `gh`…, `n` after a search, a click), the node + being connected is drawn thick in the visual color, and a dashed arrow in + that color follows the cursor. Enter links them (a line already there + stays) and connect mode is done, with the cursor back on the node `C` + started from (the user's pick). `d` removes the line between them: a link + goes; a branch (aimed at the node's parent, or at a child) is cut, and the + child comes loose with its own branch. `>` puts the node under the one + aimed at, as its last child, where it is on the canvas (a link between the + two goes: the branch takes its place). +- The `` keys style the line as you watch: `a` and `l` go through the + arrowheads (off, end, start, both) and the kinds of line (solid, dashed, + dotted, thick, double); `c` and `s` pick a color (or the node's own) and a + named line style; `t` a link's label, where typing a label that isn't + listed makes it. A line that isn't there yet is made, as a link. Everything + connect mode changes is one undo step, and Esc takes it all back. +- From visual mode, `C` connects every selected node (with `V`, each + branch's top) to the one the cursor goes to. Connect mode works in every + view, and stays in its document. +- A portal shows its document live: the root's title, its color (when the + portal has none of its own) and how many nodes it has, in the box's corner + (`?` and faded when the document isn't there). The portal remembers the + title, in case the file goes. Enter or `gf` goes through it, to the root: + a jump, so `Ctrl-o` comes back. The breadcrumb then starts with the + documents the pane came through (`Garden plans » Kitchen › Herbs`); + going back up one of them takes the trail back there. +- `p` opens the command line with `:portal ` typed (the user's + pick): a path as `:e` takes it, relative to the document's folder, with + the same completion. An existing document gets a portal under the + cursor's node, where `a` adds a child; when nothing's there, what was + typed is a new document's title, made next to this one as `:new` makes + it. A portal to its own document, or to a file that isn't a document, is + refused. +- Backlinks are put right whenever a document is opened or saved: each + document its portals lead to gets exactly one backlink per portal (an + open one in its buffer, written at once unless it has changes of its own + to save with them; a closed one in its file, unless another cumulus has + it). The root shows how many (`« 2`), the Note view lists them on the + root, and `gu` goes up to them (the user's pick): one straight there, on + the portal, several in a list, the document the pane came from first. + `:backlinks` always lists them. +- File names follow the root's title: when the title changed since the + file was read or written, the next save renames it (`-2`, `-3`… when the + name is taken; a folder's default document keeps its name). The portals + pointing at it take the new path (in the documents its backlinks name, + and any open one), as do the backlinks in the documents it leads to, the + recent documents, the capital marks and the jump lists. +- A portal whose file isn't where it says looks in that folder for its + document by uid (a file moved or renamed by hand), and keeps the path it + finds. + **Views, panes and popups** (settled in stage 7) - The Note view is made of parts: the title, the tags, the body, each link (out, then in) and each child. `j` and `k` step through them, with a bar @@ -406,7 +458,8 @@ enum Mode { // two can't disagree. Rapid entry lives here too. Visual(Visual), // anchor + Rectangle or Branches Move(Grab), // a node or a branch being moved - Connect(Connect), // the source node and the link being styled + Connect(Connect), // the nodes being connected, where the cursor goes + // back, and the undo group Esc takes back Command(CommandLine), // `:` with completion Popup(Popup), // the help, the menu, a picker or the K card (which // holds its own TextEdit while the body is edited) @@ -420,12 +473,12 @@ text). | Group | Keys | |---|---| -| **Move** (every view) | `hjkl` or arrows · `gh gj gk gl` tree moves · `w` `b` next / previous in reading order · `gg` root · `G` last · `/` `n` `N` search · `Ctrl-o` `Ctrl-i` (Tab) jump back / forward · `m{a-z}` `'{a-z}` marks (`A–Z` across documents) · Enter or `gf` through a portal · `gd` follow a cross-link | +| **Move** (every view) | `hjkl` or arrows · `gh gj gk gl` tree moves · `w` `b` next / previous in reading order · `gg` root · `G` last · `/` `n` `N` search · `Ctrl-o` `Ctrl-i` (Tab) jump back / forward · `m{a-z}` `'{a-z}` marks (`A–Z` across documents) · Enter or `gf` through a portal · `gu` up to the portals that lead here · `gd` follow a cross-link | | **View** | `zz` center · `Ctrl-e` `Ctrl-y` `zh` `zl` pan · `Ctrl-d` `Ctrl-u` half a screen · `+` `-` zoom · `za zo zc zR zM` folds · `zf` Auto · `zi` focus · `K` body card | | **Create and edit** | `o` `O` sibling below / above · `a` child · `i` `A` Enter edit the title · `I` edit from its start · `cc` `S` retype | | **Operators** | `dd` or `dab` delete branch · `dib` delete children · `x` delete just the node · `yy` `yab` `yib` yank · `p` paste as children · `P` paste as siblings below · `>>` `<<` indent / outdent · `"a`–`"z` and `"+` registers · `.` repeat · `u` `Ctrl-r` undo / redo | | **Modes** | `v` `V` visual · `M` move · `C` connect · `:` command · Esc back to normal | -| **Leader** `` | `c` color · `s` style · `B` border · `t` tags · `p` new portal · `i` inbox · `b` documents · `v` then `c`/`o`/`n` for canvas / outline / note · `w` which-key on/off · `?` help | +| **Leader** `` | `c` color · `s` style · `B` border · `t` tags · `p` new portal (`:portal `) · `i` inbox · `b` documents · `v` then `c`/`o`/`n` for canvas / outline / note · `w` which-key on/off · `?` help | | **Panes, documents** | `Ctrl-w v` `Ctrl-w s` split · `Ctrl-w hjkl` focus · `Ctrl-w w` the next pane · `Ctrl-w c` close · `Ctrl-w o` only · `Ctrl-w =` even sizes · `gt` `gT` `]b` `[b` next / previous document · `ZZ` close pane (the last one quits) | | **Outline** | `j` `k` rows · `h` `l` parent / first child · `zb` every body · `M` then `j` `k` (`J` `K`: 5) swap with siblings, `h` out, `l` under the one above | | **Note view** | `j` `k` parts · Enter `i` `A` edit the part (`I` from its start, `cc` `S` anew), or go to that link or child · `h` `l` the sibling before / after | @@ -434,7 +487,7 @@ text). | **Typing** | `Backspace` `Delete` · `Ctrl-w` delete word · `Ctrl-u` delete line · arrows, Home, End · Enter (title: done, or after `o O a` the next idea; body: new line) · Tab Shift-Tab (a new idea: under the one above / out a level) · Esc to vim keys | | **Inside text** | `h j k l w b e 0 ^ $ gg G` (and arrows, Home, End) · `f t F T ; ,` · `d` `c` `y` with any motion and `iw aw i" a" i( a(` · `x X s S D C Y` · `r ~ J p P` · `u Ctrl-r .` · `i a I A` back to typing · `o O` (title: a new sibling; body: a new line) · Esc done | | **Move mode** | `hjkl` 1 cell · `HJKL` 5 cells · `M` branch ↔ node · Enter or Esc done | -| **Connect mode** | move to the target as usual · Enter link · `d` remove · `>` attach as child · `` style menu (`s` named, `a` arrows, `l` line, `c` color, `t` label) · Esc cancel | +| **Connect mode** | move to the target as usual (or click it) · Enter link · `d` remove (a branch: cut) · `>` attach as child · `` style menu (`s` named, `a` arrows, `l` line, `c` color, `t` label) · Esc cancel (takes it all back) | | **Visual** | `d` `x` `y` `>` `<` `M` and `c` `s` `B` `t` act on the whole selection · `o` swaps ends · `v` `V` switch, or stop · Ctrl+click, Ctrl+drag pick with the mouse | ## Files @@ -616,9 +669,9 @@ cumulus/ │ command_line, theme, glyphs ├─ config/ paths (the config and data folders), settings (config.toml, `:set`), keys_file │ (keys.toml's template), theme_file (themes laid over the built-in one) - ├─ storage/ document_file (RON), write_atomically, names (file names from titles), backup, - │ lock, stamp (noticing changes made outside), memory (memory.ron), markdown - │ (nodes as Markdown, for the clipboard) + ├─ storage/ document_file (RON), write_atomically, names (file names from titles), link_paths + │ (portal paths: relative, with /), backup, lock, stamp (noticing changes made + │ outside), memory (memory.ron), markdown (nodes as Markdown, for the clipboard) └─ app/ mod.rs (App), input (keys and the mouse, routed), update (carrying actions out), render, terminal (the main loop, setup, teardown, panic hook), startup (what `cumulus` opens), documents (the open documents and their files), files @@ -628,12 +681,14 @@ cumulus/ edited, rapid entry), registers (text or nodes), clipboard (the system's, for "+), yank (nodes into registers and back), visual (selections), repeat (`.`), search (`/`), jumps (each pane's jump list), marks (capital marks, `:marks`), - panes (the split tree), popup (help, menu, pickers, card, questions); later - portals (cache, backlinks, renames) + connect (connect mode), portals (what portals show, going through them, + `:portal`, backlinks, renames), panes (the split tree), popup (help, menu, + pickers, card, questions) ``` The end-to-end tests are in `app/tests.rs` (with `app/tests/files.rs`, -`app/tests/config.rs` and `app/tests/vim.rs`): they copy the sample +`app/tests/config.rs`, `app/tests/vim.rs`, `app/tests/connect.rs` and +`app/tests/portals.rs`): they copy the sample documents to a temporary folder, type keys and click on a test terminal, and check the screen, the documents and the files. diff --git a/README.md b/README.md index a45b779..defc216 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Brainstorm in the terminal: ideas as nodes on a 2D canvas, connected, folded and color-coded, across as many documents as you like. Vim keys first; the mouse works too. Runs on Windows and Linux. -> **Status:** being built, one stage at a time. See [DESIGN.md](DESIGN.md) for -> the plan; this README grows with the app. +> **Status:** v1 is built: everything on [DESIGN.md](DESIGN.md)'s v1 list. +> v1.1 (the board, a search across documents, tidying, export) comes next. ## Build and run @@ -42,6 +42,12 @@ saves a second after each change, reloads a file another program changed A document open in another cumulus opens read-only. Each one reopens where you left it. +A file's name follows its root's title: give the root a new title and the +file is renamed (`ideas.cum` keeps its name), and the portals leading to it +follow. Portals link documents: a portal shows its document's title, color +and size, Enter goes through it, and each document knows the portals that +lead to it (`« 2` on its root, `gu` goes up to them). + ## Settings, keys and themes `:config` lists the three config files: `config.toml` (the settings), @@ -82,10 +88,13 @@ The plan for the everyday keys (all of them are in DESIGN.md): | `K` | the body in a card beside the node (`i` edits it there) | | `v` `V` | select: the nodes between here and where you started (`V`: with their branches); then `d` `y` `>` `<` `M` or `c` act on all of them | | `M` | move the branch with `hjkl` (`M` again: just the node) | +| `C` | connect: go to another node, Enter links them (`d` removes the line, `>` puts it under that node, `` `a` `l` `c` `s` `t` style it, Esc takes it all back) | | `/` `n` `N` | search: titles, tags and bodies, fuzzily, with the note of each match beside it / the next match / the previous one | | `Ctrl-o` `Tab` | back to where you jumped from / forward again (across documents) | | `ma` `'a` `mA` | mark a node / go back to it / a capital mark: from any document | | `gd` | follow a cross-link | +| `p` | a portal to another document (`:portal file`, or a new document's title) | +| Enter or `gf` `gu` | through a portal / up to the portals that lead here | | `` | the action menu: `c` color, `t` tags, `b` documents, `i` inbox, `vo` outline, `vn` note… | | `Ctrl-w v` `Ctrl-w s` | split the pane side by side / stacked (`Ctrl-w hjkl` to move between them) | | `gt` `gT` | the next / previous document | diff --git a/examples/kitchen.cum b/examples/kitchen.cum index 44bcd23..5ca2ecc 100644 --- a/examples/kitchen.cum +++ b/examples/kitchen.cum @@ -40,4 +40,11 @@ Empty it on Sundays.", ), ], + backlinks: [ + ( + from: "garden.cum", + uid: "7f3c9a1e52b04d18", + node: 10, + ), + ], ) diff --git a/src/app/action.rs b/src/app/action.rs index 45ae6ac..1e26250 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -7,6 +7,7 @@ use super::documents::BufferId; use super::jumps::Place; +use super::mode::Connect; use super::panes::Arrange; use super::popup::{MenuFor, PickKind}; use super::repeat::Look; @@ -96,6 +97,9 @@ pub enum DocAction { SetMark(char), /// Go to a mark; `'` and `` ` `` go back to before the last jump. JumpToMark(char), + /// Take back the group's undo step, if it's the newest, leaving nothing + /// to redo: Esc in connect mode. + Cancel(Group), } /// What happens around the documents. @@ -124,6 +128,21 @@ pub enum AppAction { EnterMoveMode(Vec), /// In move mode: the whole branch, or just the node. ToggleWhole, + /// Connect mode (`C`): from the cursor's node, or from these (a + /// selection's). + EnterConnectMode(Vec), + /// Back to connect mode, from a picker it opened. + ResumeConnect(Connect), + /// Enter or `gf` on a portal: through it, to its document's root. + FollowPortal(NodeId), + /// `:portal`: a portal under the cursor's node, to the document at the + /// path typed (a new document when nothing's there). + NewPortal(String), + /// `gu`: up to the portals that lead to this document; `list` + /// (`:backlinks`) lists them even when there's just one. + GoUp { + list: bool, + }, /// Back to normal mode. LeaveMode, OpenCommandLine, diff --git a/src/app/buffer.rs b/src/app/buffer.rs index 860ad96..4047598 100644 --- a/src/app/buffer.rs +++ b/src/app/buffer.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use super::action::{self, CursorAfter, DocAction, Message}; -use crate::model::{Applied, Document, History, NodeId}; +use crate::model::{Applied, Backlink, Document, History, NodeId, Portal}; /// An open document, with what goes with it while it's open. #[derive(Debug)] @@ -83,6 +83,12 @@ impl Buffer { Applied::Removed(_) | Applied::Added(_) | Applied::Changed => None, } } + DocAction::Cancel(group) => { + if self.history.cancel(group, &mut self.doc) { + self.keep_cursor(); + } + None + } DocAction::Undo(times) => self.undo_or_redo(times, true), DocAction::Redo(times) => self.undo_or_redo(times, false), DocAction::SetMark(name) => { @@ -103,6 +109,17 @@ impl Buffer { } } + /// Puts a portal node's portal in step with its document: bookkeeping, + /// outside the undo history (see [`Document::set_portal`]). + pub fn set_portal(&mut self, id: NodeId, portal: Portal) -> bool { + self.doc.set_portal(id, portal) + } + + /// Replaces the backlinks: bookkeeping, outside the undo history. + pub fn set_backlinks(&mut self, backlinks: Vec) { + self.doc.set_backlinks(backlinks); + } + /// Puts the document as read again from its file in place. When it's /// the same document, that's an undo step (`u` brings back what was on /// screen) and it returns true; another document starts a new history, diff --git a/src/app/calls.rs b/src/app/calls.rs index 2390d02..5c99fe3 100644 --- a/src/app/calls.rs +++ b/src/app/calls.rs @@ -65,7 +65,8 @@ pub fn actions_for(call: Call, ctx: &ViewContext<'_>) -> Vec { Call::Find(text) => app(AppAction::Find(text)), Call::Set(arg) => vec![Action::App(AppAction::Set(arg))], Call::Version => say(Message::info(crate::version())), - Call::Portal(_) | Call::Backlinks => later("portals come in part 8d of the build"), + Call::Portal(path) => app(AppAction::NewPortal(path)), + Call::Backlinks => app(AppAction::GoUp { list: true }), Call::Jumps => app(AppAction::OpenJumps), Call::Marks => app(AppAction::OpenMarks), Call::Registers => app(AppAction::OpenRegisters), diff --git a/src/app/commands.rs b/src/app/commands.rs index 88c2f82..b073b47 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -42,8 +42,12 @@ pub fn shared(typed: &Typed, ctx: &ViewContext<'_>) -> Option> { Command::JumpToMark(name) => vec![Action::Doc(DocAction::JumpToMark(name))], Command::FollowLink => follow_link(ctx), Command::Activate if ctx.doc.node(cursor).is_some_and(Node::is_portal) => { - say("going through a portal comes in part 8d of the build") + app(AppAction::FollowPortal(cursor)) } + Command::FollowPortal => app(AppAction::FollowPortal(cursor)), + Command::GoUp => app(AppAction::GoUp { list: false }), + // `p`: a portal, to a file typed in `:portal`. + Command::NewPortal => app(AppAction::CommandLineWith("portal ".into())), Command::EditTitle | Command::Activate => edit(EditAt::End), Command::EditTitleAtStart => edit(EditAt::Start), Command::RetypeTitle => edit(EditAt::Retype), @@ -79,6 +83,7 @@ pub fn shared(typed: &Typed, ctx: &ViewContext<'_>) -> Option> { Command::OpenAllFolds => fold_all(ctx, Fold::Open), Command::CloseAllFolds => fold_all(ctx, Fold::Closed), Command::MoveMode => app(AppAction::EnterMoveMode(Vec::new())), + Command::ConnectMode => app(AppAction::EnterConnectMode(Vec::new())), Command::Visual => app(AppAction::Visual { branches: false }), Command::VisualBranches => app(AppAction::Visual { branches: true }), Command::CommandLine => app(AppAction::OpenCommandLine), diff --git a/src/app/connect.rs b/src/app/connect.rs new file mode 100644 index 0000000..1b87ed6 --- /dev/null +++ b/src/app/connect.rs @@ -0,0 +1,328 @@ +//! Connect mode (`C`): the cursor goes to another node, a line follows it +//! there, and Enter links the two (DESIGN.md, "Keys"). On the way, `d` +//! removes the line already there (a branch between a parent and its child +//! is cut, and the child comes loose), `>` puts the node under the other +//! one, and the `` keys style the line, branch or link, as you watch. +//! Everything connect mode changes is one undo step, which Esc takes back; +//! when it's done, the cursor goes back to where it started (the user's +//! pick in part 8d). +//! +//! From visual mode, every selected node connects to the one you go to. + +use ratatui::style::Color; + +use super::action::{self, Action, AppAction, DocAction, Message}; +use super::mode::Connect; +use super::popup::{LinePick, PickKind}; +use crate::config::theme_file::{ARROWS, STROKES}; +use crate::input::command::Command; +use crate::input::engine::Typed; +use crate::model::{EditOp, Label, LineLook, LineRef, Link, NodeId, Spot}; +use crate::ui::theme::LineStyle; +use crate::view::ViewContext; + +/// Connect mode's answer to a command, or `None` for the ones that move the +/// cursor as usual. +pub fn command(typed: &Typed, connect: &Connect, ctx: &ViewContext<'_>) -> Option> { + let actions = match typed.command { + Command::Confirm => link(connect, ctx), + Command::RemoveLink => remove(connect, ctx), + Command::AttachAsChild => attach(connect, ctx), + Command::CycleArrows => cycle_arrows(connect, ctx), + Command::CycleLine => cycle_line(connect, ctx), + Command::PickColor => picker(connect, ctx, LinePick::Color), + Command::PickStyle => picker(connect, ctx, LinePick::Style), + Command::EditLabel => picker(connect, ctx, LinePick::Label), + Command::Escape => cancel(connect), + // The other end is in this document. + Command::FollowPortal | Command::JumpBack | Command::JumpForward | Command::GoUp => { + say(Message::info("connect mode stays in this document: Enter or Esc first")) + } + Command::SetMark(name) | Command::JumpToMark(name) if name.is_ascii_uppercase() => { + say(Message::info("connect mode stays in this document: Enter or Esc first")) + } + Command::Search => { + say(Message::info("n and N find the last search again here: / starts one in normal mode")) + } + _ => return None, + }; + Some(actions) +} + +/// The nodes being connected to `target`: the sources, but not the target +/// itself. +pub fn others(connect: &Connect, target: NodeId) -> Vec { + connect.sources.iter().copied().filter(|&id| id != target).collect() +} + +/// What's being connected to what: `Raised beds → Watering`. +pub fn ends(connect: &Connect, ctx: &ViewContext<'_>) -> String { + let target = ctx.cursor; + match others(connect, target).as_slice() { + [] => format!("{} → ?", what(ctx, &connect.sources)), + sources => format!("{} → {}", what(ctx, sources), title(ctx, target)), + } +} + +/// What the status bar says in connect mode, in place of the breadcrumb: +/// what's being connected to what, and what joins them already. +pub fn status(connect: &Connect, ctx: &ViewContext<'_>) -> String { + let target = ctx.cursor; + let sources = others(connect, target); + let mut text = ends(connect, ctx); + if sources.is_empty() { + text.push_str(" · go to the node to connect"); + } + if let [source] = sources.as_slice() { + match ctx.doc.line_between(*source, target) { + Some(LineRef::Link(_)) => text.push_str(" · linked"), + Some(LineRef::Branch(child)) if child == target => text.push_str(" · its child"), + Some(LineRef::Branch(_)) => text.push_str(" · its parent"), + None => {} + } + } + text +} + +/// The lines from each node being connected to the cursor's, changed by +/// `change`: a line that isn't there yet is made, as a link with that look. +pub fn restyle(connect: &Connect, ctx: &ViewContext<'_>, change: impl Fn(&mut LineLook)) -> Vec { + let target = ctx.cursor; + others(connect, target) + .into_iter() + .filter_map(|source| { + let op = match ctx.doc.line_between(source, target) { + Some(line) => { + let mut look = ctx.doc.line_look(line).cloned().unwrap_or_default(); + change(&mut look); + EditOp::SetLine { line, look } + } + None => { + let mut link = Link::new(source, target)?; + change(&mut link.look); + EditOp::Link(link) + } + }; + Some(Action::edit_in(op, connect.group)) + }) + .collect() +} + +/// The labels of the links from each node being connected to the cursor's +/// (made if they aren't there yet). A branch has no label: it's left alone. +pub fn relabel(connect: &Connect, ctx: &ViewContext<'_>, label: &Option