diff options
| -rw-r--r-- | include/ricochet/tui/document.hh | 255 | ||||
| -rw-r--r-- | include/ricochet/tui/identity.hh | 63 | ||||
| -rw-r--r-- | include/ricochet/tui/messages.hh | 233 | ||||
| -rw-r--r-- | include/ricochet/tui/position.hh | 116 | ||||
| -rw-r--r-- | include/ricochet/tui/view.hh | 69 | ||||
| -rw-r--r-- | src/tui/document.cc | 30 |
6 files changed, 766 insertions, 0 deletions
diff --git a/include/ricochet/tui/document.hh b/include/ricochet/tui/document.hh new file mode 100644 index 0000000..bd76442 --- /dev/null +++ b/include/ricochet/tui/document.hh @@ -0,0 +1,255 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "ricochet/tui/position.hh" + +#include <cstddef> +#include <cstdint> +#include <memory> +#include <optional> +#include <string> +#include <variant> +#include <vector> + +namespace ricochet::tui { + +struct TextStyle +{ + bool emphasis { false }; + bool strong { false }; + bool code { false }; + bool subdued { false }; + bool warning { false }; +}; + +struct StyledRange +{ + ByteRange bytes {}; + TextStyle style {}; +}; + +// Leaf IDs and structural block IDs share one document-local namespace. +// All text offsets address a Text leaf, not a concatenation of rendered lines. +struct Text +{ + BlockId id {}; + // LF is an explicit hard break; soft wrapping never changes this string. + std::string utf8 {}; + std::vector<StyledRange> styles {}; +}; + +struct ControlPlaceholder +{ ControlId control {}; }; + +using Inline = std::variant<Text, ControlPlaceholder>; + +struct Paragraph +{ std::vector<Inline> content {}; }; + +struct Heading +{ + unsigned int level { 1 }; + std::vector<Inline> content {}; +}; + +struct Preformatted +{ + Text content {}; + std::optional<std::string> language {}; +}; + +struct Block; + +struct ListItem +{ + std::optional<std::string> marker {}; + std::vector<Block> content {}; +}; + +enum class ListKind : std::uint8_t +{ + Unordered, + Ordered +}; + +struct List +{ + ListKind kind { ListKind::Unordered }; + std::int64_t first_number { 1 }; + std::vector<ListItem> items {}; +}; + +struct TableCell +{ + // The producer resolves HTML spans to grid coordinates before delivery. + std::size_t column { 0 }; + std::size_t column_span { 1 }; + std::size_t row_span { 1 }; + bool header { false }; + std::vector<Block> content {}; +}; + +struct TableRow +{ std::vector<TableCell> cells {}; }; + +struct Table +{ + std::size_t columns { 0 }; + std::vector<TableRow> rows {}; +}; + +struct Alternative +{ Text description {}; }; + +struct Separator +{}; + +struct Block +{ + // Structural identity supports empty blocks and per-block horizontal scroll. + BlockId id {}; + std::variant<Paragraph, Heading, Preformatted, List, Table, ControlPlaceholder, Alternative, Separator> + content {}; +}; + +struct Link +{ + LinkId id {}; + std::string target {}; + std::string description {}; + // Multiple ranges can belong to one link, independently of text styling. + std::vector<TextRange> ranges {}; +}; + +enum class TextControlKind : std::uint8_t +{ + SingleLine, + MultiLine, + Password +}; + +struct TextControl +{ + TextControlKind kind { TextControlKind::SingleLine }; + std::string value {}; + std::string default_value {}; +}; + +struct Checkbox +{ + bool checked { false }; + bool default_checked { false }; +}; + +struct RadioButton +{ + RadioGroupId group {}; + bool checked { false }; + bool default_checked { false }; +}; + +struct SelectOption +{ + OptionId id {}; + std::string label {}; + bool disabled { false }; +}; + +enum class SelectKind : std::uint8_t +{ + Single, + Multiple +}; + +struct SelectControl +{ + SelectKind kind { SelectKind::Single }; + std::vector<SelectOption> options {}; + std::vector<OptionId> selected {}; + std::vector<OptionId> default_selected {}; +}; + +enum class ButtonKind : std::uint8_t +{ + Submit, + Reset +}; + +struct Button +{ ButtonKind kind { ButtonKind::Submit }; }; + +struct UnsupportedControl +{ std::string description {}; }; + +struct Control +{ + ControlId id {}; + std::optional<FormId> form {}; + std::string label {}; + std::string hint {}; + std::optional<std::string> error {}; + bool disabled { false }; + bool read_only { false }; + std::variant<TextControl, Checkbox, RadioButton, SelectControl, Button, UnsupportedControl> content {}; +}; + +struct Form +{ + FormId id {}; + std::string label {}; +}; + +struct Anchor +{ + std::string name {}; + ContentPoint point {}; +}; + +struct DocumentMetadata +{ + std::string title {}; + std::string display_url {}; + std::optional<std::string> language {}; +}; + +// Producers build owned values. No DOM pointers, terminal codes or screen coordinates. +struct DocumentData +{ + DocumentVersion version {}; + DocumentMetadata metadata {}; + std::vector<Block> blocks {}; + std::vector<Link> links {}; + std::vector<Control> controls {}; + std::vector<Form> forms {}; + std::vector<Anchor> anchors {}; +}; + +class Document +{ +public: + explicit Document( DocumentData data ); + + const DocumentData& data() const noexcept; + +private: + std::shared_ptr<const DocumentData> data_; +}; + +} // namespace ricochet::tui diff --git a/include/ricochet/tui/identity.hh b/include/ricochet/tui/identity.hh new file mode 100644 index 0000000..624c6cc --- /dev/null +++ b/include/ricochet/tui/identity.hh @@ -0,0 +1,63 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <compare> +#include <cstdint> + +namespace ricochet::tui { + +template<class Tag> +struct Identity +{ + std::uint64_t value { 0 }; + + auto operator<=>( const Identity& ) const = default; +}; + +using TabId = Identity<struct TabTag>; +using DocumentId = Identity<struct DocumentTag>; +using Revision = Identity<struct RevisionTag>; +using DraftRevision = Identity<struct DraftRevisionTag>; +using BlockId = Identity<struct BlockTag>; +using LinkId = Identity<struct LinkTag>; +using ControlId = Identity<struct ControlTag>; +using FormId = Identity<struct FormTag>; +using OptionId = Identity<struct OptionTag>; +using RadioGroupId = Identity<struct RadioGroupTag>; +using TaskId = Identity<struct TaskTag>; +using RequestId = Identity<struct RequestTag>; + +struct DocumentVersion +{ + DocumentId document {}; + Revision revision {}; + + bool operator==( const DocumentVersion& ) const = default; +}; + +struct DocumentScope +{ + TabId tab {}; + DocumentVersion version {}; + + bool operator==( const DocumentScope& ) const = default; +}; + +} // namespace ricochet::tui diff --git a/include/ricochet/tui/messages.hh b/include/ricochet/tui/messages.hh new file mode 100644 index 0000000..e7dc417 --- /dev/null +++ b/include/ricochet/tui/messages.hh @@ -0,0 +1,233 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "ricochet/tui/document.hh" +#include "ricochet/tui/view.hh" + +#include <cstdint> +#include <optional> +#include <string> +#include <variant> +#include <vector> + +namespace ricochet::tui { + +struct TabInfo +{ + TabId id {}; + std::string title {}; + std::string display_url {}; + std::optional<TaskId> loading_task {}; +}; + +struct SyncTabs +{ + // Vector order is tab order. Request IDs acknowledge create/close actions. + std::vector<TabInfo> tabs {}; + std::optional<TabId> active {}; + std::vector<RequestId> completed_requests {}; +}; + +struct NavigationStarted +{ + TabId tab {}; + RequestId request {}; + TaskId task {}; + std::string display_target {}; +}; + +struct CommitDocument +{ + TabId tab {}; + RequestId navigation {}; + Document document; + std::optional<ViewSnapshot> restore {}; +}; + +struct TextCorrespondence +{ + // The producer identifies identical text in the two revisions, including every + // changed portion crossed by a selection. This is not an inferred text diff. + TextRange source {}; + TextRange target {}; +}; + +struct PositionMapping +{ + DocumentVersion source {}; + DocumentVersion target {}; + std::vector<TextCorrespondence> text {}; +}; + +enum class ViewRetention : std::uint8_t +{ + Reset, + PreserveUnchanged +}; + +struct UpdateDocument +{ + TabId tab {}; + RequestId navigation {}; + DocumentVersion source {}; + Document document; + ViewRetention retention { ViewRetention::PreserveUnchanged }; + std::optional<PositionMapping> mapping {}; +}; + +struct LocateView +{ + DocumentScope scope {}; + std::variant<std::string, DocumentPoint> target {}; +}; + +// Values are keyed by control identity, never by field name or action URL. +// These business messages may carry passwords; ViewSnapshot deliberately does not. +using ControlValue = std::variant<std::string, bool, std::vector<OptionId>>; + +struct ControlDraft +{ + ControlId control {}; + ControlValue value {}; +}; + +struct FieldError +{ + ControlId control {}; + std::string message {}; +}; + +struct FormFeedback +{ + DocumentScope scope {}; + FormId form {}; + RequestId request {}; + DraftRevision draft_revision {}; + std::vector<FieldError> errors {}; + std::vector<ControlDraft> normalized_values {}; +}; + +struct ActionFailed +{ + RequestId request {}; + std::optional<TabId> tab {}; + std::string summary {}; + std::optional<std::string> details {}; +}; + +struct ExitDecision +{ + RequestId request {}; + bool approved { false }; + std::optional<std::string> explanation {}; +}; + +using HostUpdate = std::variant<SyncTabs, + NavigationStarted, + CommitDocument, + UpdateDocument, + LocateView, + FormFeedback, + ActionFailed, + ExitDecision>; + +enum class OpenTarget : std::uint8_t +{ + CurrentTab, + NewTab +}; + +struct OpenLocation +{ + RequestId request {}; + TabId tab {}; + std::string input {}; + OpenTarget target { OpenTarget::CurrentTab }; +}; + +struct OpenLink +{ + RequestId request {}; + DocumentScope source {}; + LinkId link {}; + std::string target {}; + OpenTarget destination { OpenTarget::CurrentTab }; +}; + +enum class NavigationCommand : std::uint8_t +{ + Back, + Forward, + Reload +}; + +struct Navigate +{ + RequestId request {}; + TabId tab {}; + std::optional<DocumentVersion> document {}; + NavigationCommand command { NavigationCommand::Reload }; +}; + +struct CreateTab +{ RequestId request {}; }; + +struct CloseTab +{ + RequestId request {}; + TabId tab {}; +}; + +enum class FormOperation : std::uint8_t +{ + Submit, + Reset +}; + +struct FormAction +{ + RequestId request {}; + DocumentScope scope {}; + FormId form {}; + std::optional<ControlId> button {}; + DraftRevision draft_revision {}; + FormOperation operation { FormOperation::Submit }; + std::vector<ControlDraft> values {}; +}; + +struct ExportSelection +{ + RequestId request {}; + DocumentScope source {}; + // Already extracted from one view snapshot; passwords are masked. + std::string text {}; +}; + +struct RequestExit +{ + RequestId request {}; + std::optional<TabId> active {}; + std::vector<ViewSnapshot> views {}; +}; + +using UserAction + = std::variant<OpenLocation, OpenLink, Navigate, CreateTab, CloseTab, FormAction, ExportSelection, RequestExit>; + +} // namespace ricochet::tui diff --git a/include/ricochet/tui/position.hh b/include/ricochet/tui/position.hh new file mode 100644 index 0000000..49b63f2 --- /dev/null +++ b/include/ricochet/tui/position.hh @@ -0,0 +1,116 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "ricochet/tui/identity.hh" + +#include <cstddef> +#include <cstdint> +#include <variant> + +namespace ricochet::tui { + +// UTF-8 byte ranges are half-open. Text operations use grapheme boundaries. +struct ByteRange +{ + std::size_t start { 0 }; + std::size_t end { 0 }; + + bool operator==( const ByteRange& ) const = default; +}; + +struct TextPosition +{ + BlockId block {}; + std::size_t byte_offset { 0 }; + + bool operator==( const TextPosition& ) const = default; +}; + +struct ControlTextPosition +{ + ControlId control {}; + DraftRevision revision {}; + // Offset in display text; password offsets refer only to its mask. + std::size_t byte_offset { 0 }; + + bool operator==( const ControlTextPosition& ) const = default; +}; + +enum class BoundarySide : std::uint8_t +{ + Before, + After +}; + +struct ObjectPosition +{ + std::variant<BlockId, ControlId> object {}; + BoundarySide side { BoundarySide::Before }; + + bool operator==( const ObjectPosition& ) const = default; +}; + +struct DocumentStart +{ bool operator==( const DocumentStart& ) const = default; }; + +// Local to a document snapshot. Stored anchors cannot depend on mutable drafts. +using ContentPoint = std::variant<DocumentStart, TextPosition, ObjectPosition>; +using DocumentPoint = std::variant<DocumentStart, TextPosition, ControlTextPosition, ObjectPosition>; + +struct LogicalPosition +{ + DocumentScope scope {}; + DocumentPoint point {}; + + bool operator==( const LogicalPosition& ) const = default; +}; + +// Endpoints follow reading order, never the numeric order of their IDs. +struct DocumentRange +{ + DocumentPoint start {}; + DocumentPoint end {}; + + bool operator==( const DocumentRange& ) const = default; +}; + +struct TextRange +{ + BlockId block {}; + ByteRange bytes {}; + + bool operator==( const TextRange& ) const = default; +}; + +enum class WrapAffinity : std::uint8_t +{ + PreviousLine, + NextLine +}; + +struct DisplayPosition +{ + DocumentPoint point {}; + WrapAffinity affinity { WrapAffinity::NextLine }; + + bool operator==( const DisplayPosition& ) const = default; +}; + +} // namespace ricochet::tui diff --git a/include/ricochet/tui/view.hh b/include/ricochet/tui/view.hh new file mode 100644 index 0000000..d259cf3 --- /dev/null +++ b/include/ricochet/tui/view.hh @@ -0,0 +1,69 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "ricochet/tui/position.hh" + +#include <cstddef> +#include <cstdint> +#include <optional> +#include <string> +#include <vector> + +namespace ricochet::tui { + +enum class SelectionDirection : std::uint8_t +{ + Forward, + Backward +}; + +struct SelectionSnapshot +{ + // The normalized half-open range also covers any controls between its endpoints. + DocumentRange range {}; + SelectionDirection direction { SelectionDirection::Forward }; +}; + +struct HorizontalScroll +{ + BlockId block {}; + std::size_t columns { 0 }; +}; + +struct SearchSnapshot +{ + std::string query {}; + std::optional<DocumentRange> current_match {}; +}; + +// All positions and ranges belong to scope and draft_revision. +// Editing sessions and control values (including passwords) are not exported here. +struct ViewSnapshot +{ + DocumentScope scope {}; + DraftRevision draft_revision {}; + DisplayPosition cursor {}; + DisplayPosition viewport_anchor {}; + std::optional<SelectionSnapshot> selection {}; + std::vector<HorizontalScroll> horizontal_scroll {}; + SearchSnapshot search {}; +}; + +} // namespace ricochet::tui diff --git a/src/tui/document.cc b/src/tui/document.cc new file mode 100644 index 0000000..335c6d6 --- /dev/null +++ b/src/tui/document.cc @@ -0,0 +1,30 @@ +/* + * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation. + * Copyright (C) 2026 Richard Qin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ricochet/tui/document.hh" + +#include <utility> + +namespace ricochet::tui { + +Document::Document( DocumentData data ) : data_( std::make_shared<const DocumentData>( std::move( data ) ) ) {} + +const DocumentData& Document::data() const noexcept +{ return *data_; } + +} // namespace ricochet::tui |
