feat: posts open in the rendered markdown preview by default, ctrl+e toggles to the editor
This commit is contained in:
@@ -21,9 +21,10 @@ defmodule BDS.TUI do
|
||||
`yyyy-mm-dd`; enter keeps the filter, esc clears it),
|
||||
`:` vi-style command prompt over the Blog-menu shell commands, `?`
|
||||
command help (commands whose report/apply UI is GUI-only are marked).
|
||||
Editor focus: text keys edit, `ctrl+s` save, `ctrl+p` publish,
|
||||
`ctrl+e` word-wrapped preview, `ctrl+t` edit title, `ctrl+l` cycle
|
||||
language, `ctrl+g` AI suggestions, `esc` back to sidebar. `ctrl+q`
|
||||
Posts open in the rendered markdown preview (new empty posts open in
|
||||
the editor). Editor focus: `ctrl+e` toggles preview/editor, text keys
|
||||
edit, `ctrl+s` save, `ctrl+p` publish, `ctrl+t` edit title, `ctrl+l`
|
||||
cycle language, `ctrl+g` AI suggestions, `esc` back to sidebar. `ctrl+q`
|
||||
quits, `esc` also closes a media preview. The UI locale is the
|
||||
server-side UI language setting.
|
||||
"""
|
||||
@@ -215,7 +216,9 @@ defmodule BDS.TUI do
|
||||
when is_binary(project_id) do
|
||||
case Posts.create_post(%{project_id: project_id, title: "", content: ""}) do
|
||||
{:ok, post} ->
|
||||
{:noreply, state |> load_sidebar() |> open_post(post.id)}
|
||||
# A fresh, empty post starts in the editor — there is nothing to
|
||||
# preview yet; existing posts open in the preview instead.
|
||||
{:noreply, state |> load_sidebar() |> open_post(post.id, false)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:noreply, toast(state, dgettext("ui", "New Post"), inspect(reason))}
|
||||
@@ -691,9 +694,6 @@ defmodule BDS.TUI do
|
||||
defp editor_key(%{code: "esc"}, %{editor: %{editing_title?: true}} = state),
|
||||
do: {:noreply, put_in(state.editor.editing_title?, false)}
|
||||
|
||||
defp editor_key(%{code: "esc"}, %{editor: %{preview?: true}} = state),
|
||||
do: {:noreply, put_in(state.editor.preview?, false)}
|
||||
|
||||
defp editor_key(%{code: "esc"}, state), do: {:noreply, %{state | focus: :sidebar}}
|
||||
|
||||
defp editor_key(%{code: "e", modifiers: ["ctrl"]}, state),
|
||||
@@ -1470,7 +1470,9 @@ defmodule BDS.TUI do
|
||||
|
||||
# ── Post editor ──────────────────────────────────────────────────────────
|
||||
|
||||
defp open_post(state, post_id) do
|
||||
# Posts open in the rendered markdown preview by default — the editor is
|
||||
# one ctrl+e away. New (empty) posts pass preview?: false.
|
||||
defp open_post(state, post_id, preview? \\ true) do
|
||||
case Posts.get_post(post_id) do
|
||||
nil ->
|
||||
toast(state, dgettext("ui", "Posts"), dgettext("ui", "Post not found."))
|
||||
@@ -1478,11 +1480,17 @@ defmodule BDS.TUI do
|
||||
post ->
|
||||
metadata = Metadata.project_metadata(post.project_id)
|
||||
language = Metadata.canonical_language(post, metadata)
|
||||
%{state | editor: build_editor(post, metadata, language), focus: :editor, image: nil}
|
||||
|
||||
%{
|
||||
state
|
||||
| editor: build_editor(post, metadata, language, preview?),
|
||||
focus: :editor,
|
||||
image: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_editor(post, metadata, language) do
|
||||
defp build_editor(post, metadata, language, preview?) do
|
||||
form = Draft.persisted_form(post, metadata, language)
|
||||
textarea = ExRatatui.textarea_new()
|
||||
:ok = ExRatatui.textarea_insert_str(textarea, form["content"] || "")
|
||||
@@ -1495,7 +1503,7 @@ defmodule BDS.TUI do
|
||||
title: form["title"] || "",
|
||||
textarea: textarea,
|
||||
editing_title?: false,
|
||||
preview?: false,
|
||||
preview?: preview?,
|
||||
dirty: false
|
||||
}
|
||||
end
|
||||
@@ -1505,7 +1513,7 @@ defmodule BDS.TUI do
|
||||
defp reload_editor(%{editor: editor} = state) do
|
||||
case Posts.get_post(editor.post.id) do
|
||||
nil -> %{state | editor: nil, focus: :sidebar}
|
||||
post -> %{state | editor: build_editor(post, editor.metadata, editor.language)}
|
||||
post -> %{state | editor: build_editor(post, editor.metadata, editor.language, editor.preview?)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1541,7 +1549,7 @@ defmodule BDS.TUI do
|
||||
languages = Metadata.languages(editor.metadata)
|
||||
index = Enum.find_index(languages, &(&1 == editor.language)) || 0
|
||||
next = Enum.at(languages, rem(index + 1, length(languages)))
|
||||
%{state | editor: build_editor(editor.post, editor.metadata, next)}
|
||||
%{state | editor: build_editor(editor.post, editor.metadata, next, editor.preview?)}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -33,9 +33,12 @@ rule SidebarNavigation {
|
||||
|
||||
rule OpenEntry {
|
||||
when: TuiKeyPressed(code: "enter")
|
||||
-- Posts open in the editor (title + textarea seeded from the
|
||||
-- persisted form). Images open in the terminal image preview.
|
||||
-- Other entities report that terminal editing is not available yet.
|
||||
-- Posts open in the rendered markdown preview by default (title +
|
||||
-- textarea still seeded from the persisted form; ctrl+e switches to
|
||||
-- editing). New empty posts created with "n" open in the editor
|
||||
-- instead — there is nothing to preview yet. Images open in the
|
||||
-- terminal image preview. Other entities report that terminal
|
||||
-- editing is not available yet.
|
||||
ensures: TuiState.editing_post.updated()
|
||||
}
|
||||
|
||||
@@ -49,9 +52,12 @@ rule SaveAndPublish {
|
||||
|
||||
rule WrappedPreview {
|
||||
when: TuiKeyPressed(code: "e", modifiers: "ctrl")
|
||||
-- The editing textarea cannot soft-wrap (upstream ratatui-textarea
|
||||
-- limitation), so ctrl+e toggles a read-only word-wrapped Markdown
|
||||
-- preview of the current draft; keys in preview never edit content.
|
||||
-- ctrl+e toggles between the read-only word-wrapped Markdown
|
||||
-- preview (the default mode when opening a post — rendered through
|
||||
-- tui-markdown so headings/emphasis/lists are styled) and the
|
||||
-- editing textarea (which cannot soft-wrap, an upstream
|
||||
-- ratatui-textarea limitation); keys in preview never edit content,
|
||||
-- and esc returns to the sidebar from either mode.
|
||||
ensures: PreviewToggled()
|
||||
}
|
||||
|
||||
|
||||
@@ -68,13 +68,27 @@ defmodule BDS.TUITest do
|
||||
assert text =~ "Hello TUI"
|
||||
end
|
||||
|
||||
test "navigates and opens a post in the editor", %{post: post} do
|
||||
test "opening a post lands in the markdown preview, not the editor", %{post: post} do
|
||||
{:ok, _} =
|
||||
BDS.Posts.update_post(post.id, %{content: "# Big Heading\n\nsome **bold** words"})
|
||||
|
||||
state = mount!() |> press("enter")
|
||||
|
||||
assert state.focus == :editor
|
||||
assert state.editor.post.id == post.id
|
||||
assert ExRatatui.textarea_get_value(state.editor.textarea) == "terminal body"
|
||||
assert screen_text(state) =~ "terminal body"
|
||||
assert state.editor.preview?
|
||||
|
||||
text = screen_text(state)
|
||||
assert text =~ "Big Heading"
|
||||
# Rendered markdown: inline markers are consumed by the styling
|
||||
# (headings keep their # but get styled, invisible in a text dump).
|
||||
assert text =~ "some bold words"
|
||||
refute text =~ "**bold**"
|
||||
|
||||
# ctrl+e drops into the editing textarea with the raw source.
|
||||
state = press(state, "e", ["ctrl"])
|
||||
refute state.editor.preview?
|
||||
assert ExRatatui.textarea_get_value(state.editor.textarea) =~ "**bold**"
|
||||
end
|
||||
|
||||
test "ctrl+s persists textarea edits", %{post: post} do
|
||||
@@ -95,13 +109,16 @@ defmodule BDS.TUITest do
|
||||
assert state.editor.post.status == :published
|
||||
end
|
||||
|
||||
test "n creates a new draft post", %{project: project} do
|
||||
test "n creates a new draft post and starts in the editor, not the preview", %{
|
||||
project: project
|
||||
} do
|
||||
count_before = post_count(project.id)
|
||||
state = mount!() |> press("n")
|
||||
|
||||
assert post_count(project.id) == count_before + 1
|
||||
assert state.focus == :editor
|
||||
assert state.editor.post.status == :draft
|
||||
refute state.editor.preview?
|
||||
end
|
||||
|
||||
test "esc returns focus to the sidebar" do
|
||||
@@ -109,11 +126,11 @@ defmodule BDS.TUITest do
|
||||
assert state.focus == :sidebar
|
||||
end
|
||||
|
||||
test "ctrl+e toggles a word-wrapped preview of the draft", %{post: post} do
|
||||
test "the preview word-wraps and ctrl+e toggles back and forth", %{post: post} do
|
||||
long_line = String.duplicate("lorem ipsum dolor sit amet ", 8) <> "FINALWORD"
|
||||
{:ok, _} = BDS.Posts.update_post(post.id, %{content: long_line})
|
||||
|
||||
state = mount!() |> press("enter") |> press("e", ["ctrl"])
|
||||
state = mount!() |> press("enter")
|
||||
assert state.editor.preview?
|
||||
|
||||
# The textarea cannot soft-wrap, so the tail of a long line is off-screen
|
||||
@@ -122,10 +139,14 @@ defmodule BDS.TUITest do
|
||||
|
||||
state = press(state, "e", ["ctrl"])
|
||||
refute state.editor.preview?
|
||||
|
||||
state = press(state, "e", ["ctrl"])
|
||||
assert state.editor.preview?
|
||||
end
|
||||
|
||||
test "keys in preview mode do not edit the content" do
|
||||
state = mount!() |> press("enter") |> press("e", ["ctrl"])
|
||||
state = mount!() |> press("enter")
|
||||
assert state.editor.preview?
|
||||
before = ExRatatui.textarea_get_value(state.editor.textarea)
|
||||
|
||||
state = press(state, "x")
|
||||
@@ -133,11 +154,12 @@ defmodule BDS.TUITest do
|
||||
refute state.editor.dirty
|
||||
end
|
||||
|
||||
test "esc in preview returns to editing, not to the sidebar" do
|
||||
state = mount!() |> press("enter") |> press("e", ["ctrl"]) |> press("esc")
|
||||
test "esc returns to the sidebar from both preview and editor" do
|
||||
state = mount!() |> press("enter") |> press("esc")
|
||||
assert state.focus == :sidebar
|
||||
|
||||
refute state.editor.preview?
|
||||
assert state.focus == :editor
|
||||
state = state |> press("enter") |> press("e", ["ctrl"]) |> press("esc")
|
||||
assert state.focus == :sidebar
|
||||
end
|
||||
|
||||
test "entity_changed refreshes the sidebar", %{project: project} do
|
||||
|
||||
Reference in New Issue
Block a user