From eb6509c6f3eddbd00ac59d89892209518ede53f5 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Sat, 17 Oct 2026 03:36:29 +0800 Subject: feat(tui): add document model and host message interfaces --- include/ricochet/tui/document.hh | 255 +++++++++++++++++++++++++++++++++++++++ include/ricochet/tui/identity.hh | 63 ++++++++++ include/ricochet/tui/messages.hh | 233 +++++++++++++++++++++++++++++++++++ include/ricochet/tui/position.hh | 116 ++++++++++++++++++ include/ricochet/tui/view.hh | 69 +++++++++++ src/tui/document.cc | 30 +++++ 6 files changed, 766 insertions(+) create mode 100644 include/ricochet/tui/document.hh create mode 100644 include/ricochet/tui/identity.hh create mode 100644 include/ricochet/tui/messages.hh create mode 100644 include/ricochet/tui/position.hh create mode 100644 include/ricochet/tui/view.hh create mode 100644 src/tui/document.cc 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 . + */ + +#pragma once + +#include "ricochet/tui/position.hh" + +#include +#include +#include +#include +#include +#include +#include + +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 styles {}; +}; + +struct ControlPlaceholder +{ ControlId control {}; }; + +using Inline = std::variant; + +struct Paragraph +{ std::vector content {}; }; + +struct Heading +{ + unsigned int level { 1 }; + std::vector content {}; +}; + +struct Preformatted +{ + Text content {}; + std::optional language {}; +}; + +struct Block; + +struct ListItem +{ + std::optional marker {}; + std::vector content {}; +}; + +enum class ListKind : std::uint8_t +{ + Unordered, + Ordered +}; + +struct List +{ + ListKind kind { ListKind::Unordered }; + std::int64_t first_number { 1 }; + std::vector 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 content {}; +}; + +struct TableRow +{ std::vector cells {}; }; + +struct Table +{ + std::size_t columns { 0 }; + std::vector rows {}; +}; + +struct Alternative +{ Text description {}; }; + +struct Separator +{}; + +struct Block +{ + // Structural identity supports empty blocks and per-block horizontal scroll. + BlockId id {}; + std::variant + content {}; +}; + +struct Link +{ + LinkId id {}; + std::string target {}; + std::string description {}; + // Multiple ranges can belong to one link, independently of text styling. + std::vector 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 options {}; + std::vector selected {}; + std::vector 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 form {}; + std::string label {}; + std::string hint {}; + std::optional error {}; + bool disabled { false }; + bool read_only { false }; + std::variant 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 language {}; +}; + +// Producers build owned values. No DOM pointers, terminal codes or screen coordinates. +struct DocumentData +{ + DocumentVersion version {}; + DocumentMetadata metadata {}; + std::vector blocks {}; + std::vector links {}; + std::vector controls {}; + std::vector
forms {}; + std::vector anchors {}; +}; + +class Document +{ +public: + explicit Document( DocumentData data ); + + const DocumentData& data() const noexcept; + +private: + std::shared_ptr 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 . + */ + +#pragma once + +#include +#include + +namespace ricochet::tui { + +template +struct Identity +{ + std::uint64_t value { 0 }; + + auto operator<=>( const Identity& ) const = default; +}; + +using TabId = Identity; +using DocumentId = Identity; +using Revision = Identity; +using DraftRevision = Identity; +using BlockId = Identity; +using LinkId = Identity; +using ControlId = Identity; +using FormId = Identity; +using OptionId = Identity; +using RadioGroupId = Identity; +using TaskId = Identity; +using RequestId = Identity; + +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 . + */ + +#pragma once + +#include "ricochet/tui/document.hh" +#include "ricochet/tui/view.hh" + +#include +#include +#include +#include +#include + +namespace ricochet::tui { + +struct TabInfo +{ + TabId id {}; + std::string title {}; + std::string display_url {}; + std::optional loading_task {}; +}; + +struct SyncTabs +{ + // Vector order is tab order. Request IDs acknowledge create/close actions. + std::vector tabs {}; + std::optional active {}; + std::vector completed_requests {}; +}; + +struct NavigationStarted +{ + TabId tab {}; + RequestId request {}; + TaskId task {}; + std::string display_target {}; +}; + +struct CommitDocument +{ + TabId tab {}; + RequestId navigation {}; + Document document; + std::optional 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 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 mapping {}; +}; + +struct LocateView +{ + DocumentScope scope {}; + std::variant 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>; + +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 errors {}; + std::vector normalized_values {}; +}; + +struct ActionFailed +{ + RequestId request {}; + std::optional tab {}; + std::string summary {}; + std::optional details {}; +}; + +struct ExitDecision +{ + RequestId request {}; + bool approved { false }; + std::optional explanation {}; +}; + +using HostUpdate = std::variant; + +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 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 button {}; + DraftRevision draft_revision {}; + FormOperation operation { FormOperation::Submit }; + std::vector 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 active {}; + std::vector views {}; +}; + +using UserAction + = std::variant; + +} // 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 . + */ + +#pragma once + +#include "ricochet/tui/identity.hh" + +#include +#include +#include + +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 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; +using DocumentPoint = std::variant; + +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 . + */ + +#pragma once + +#include "ricochet/tui/position.hh" + +#include +#include +#include +#include +#include + +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 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 selection {}; + std::vector 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 . + */ + +#include "ricochet/tui/document.hh" + +#include + +namespace ricochet::tui { + +Document::Document( DocumentData data ) : data_( std::make_shared( std::move( data ) ) ) {} + +const DocumentData& Document::data() const noexcept +{ return *data_; } + +} // namespace ricochet::tui -- cgit v1.2.3