From aab7a94ae29cc3b10fd0d1b34ae5c1f9fef46bec Mon Sep 17 00:00:00 2001 From: Nanaloveyuki Date: Tue, 11 Aug 2026 13:30:58 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20bounded=20history=20sink=20(#?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/README-en.md | 2 +- docs/api/history-sink-capacity.md | 19 +++++++ docs/api/history-sink-clear.md | 20 +++++++ docs/api/history-sink-count.md | 19 +++++++ docs/api/history-sink-dropped-count.md | 20 +++++++ docs/api/history-sink-snapshot.md | 20 +++++++ docs/api/history-sink-type.md | 31 +++++++++++ docs/api/history-sink.md | 45 ++++++++++++++++ docs/changes/1.2.0(0.8.0).md | 15 ++++++ docs/changes/index.md | 1 + docs/examples/console.md | 2 +- docs/zh/examples/console.md | 2 +- examples/basic/pkg.generated.mbti | 1 - examples/config_build/pkg.generated.mbti | 1 - examples/console_basic/pkg.generated.mbti | 1 - examples/file_rotation/pkg.generated.mbti | 1 - examples/presets/pkg.generated.mbti | 1 - examples/style_tags/pkg.generated.mbti | 1 - examples/text_formatter/pkg.generated.mbti | 1 - moon.mod | 2 +- src-async/pkg.generated.mbti | 1 - src-async/utils/pkg.generated.mbti | 1 - src/config_model/pkg.generated.mbti | 1 - src/core/pkg.generated.mbti | 1 - src/file_model/pkg.generated.mbti | 1 - src/file_runtime/pkg.generated.mbti | 1 - src/formatting/pkg.generated.mbti | 1 - src/pkg.generated.mbti | 5 +- src/presets_pkg/pkg.generated.mbti | 1 - src/queue_model/pkg.generated.mbti | 1 - src/record_ops/pkg.generated.mbti | 1 - src/runtime/pkg.generated.mbti | 1 - src/sink_graph/pkg.generated.mbti | 15 +++++- src/sink_graph/sinks.mbt | 63 ++++++++++++++++++++++ src/sink_graph_surface_test.mbt | 34 ++++++++++++ src/sinks.mbt | 8 +++ src/utils/pkg.generated.mbti | 1 - 38 files changed, 318 insertions(+), 26 deletions(-) create mode 100644 docs/api/history-sink-capacity.md create mode 100644 docs/api/history-sink-clear.md create mode 100644 docs/api/history-sink-count.md create mode 100644 docs/api/history-sink-dropped-count.md create mode 100644 docs/api/history-sink-snapshot.md create mode 100644 docs/api/history-sink-type.md create mode 100644 docs/api/history-sink.md create mode 100644 docs/changes/1.2.0(0.8.0).md diff --git a/README.md b/README.md index de68467..05a23aa 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ BitLogger 的文档按使用路径组织:先完成一个可运行的日志输 ```bash moon new log-demo cd log-demo -moon add Nanaloveyuki/BitLogger@0.7.3 +moon add Nanaloveyuki/BitLogger@0.8.0 ``` ### 2. 写入第一条结构化日志 diff --git a/docs/README-en.md b/docs/README-en.md index 3e68c7d..db7d09b 100644 --- a/docs/README-en.md +++ b/docs/README-en.md @@ -18,7 +18,7 @@ The documentation is organized around complete usage flows. Start with an execut ```bash moon new log-demo cd log-demo -moon add Nanaloveyuki/BitLogger@0.7.3 +moon add Nanaloveyuki/BitLogger@0.8.0 ``` ### 2. Import And Log diff --git a/docs/api/history-sink-capacity.md b/docs/api/history-sink-capacity.md new file mode 100644 index 0000000..bd17c2f --- /dev/null +++ b/docs/api/history-sink-capacity.md @@ -0,0 +1,19 @@ +--- +name: history-sink-capacity +group: api +category: sink +update-time: 20260811 +description: Read the normalized retention capacity of a HistorySink. +key-word: + - sink + - history + - capacity +--- + +## History-sink-capacity + +```moonbit +pub fn HistorySink::capacity(self : HistorySink) -> Int +``` + +Returns the positive retention limit selected when the sink was constructed. diff --git a/docs/api/history-sink-clear.md b/docs/api/history-sink-clear.md new file mode 100644 index 0000000..9d0b9f3 --- /dev/null +++ b/docs/api/history-sink-clear.md @@ -0,0 +1,20 @@ +--- +name: history-sink-clear +group: api +category: sink +update-time: 20260811 +description: Clear a HistorySink and reset its eviction counter. +key-word: + - sink + - history + - clear +--- + +## History-sink-clear + +```moonbit +pub fn HistorySink::clear(self : HistorySink) -> Unit +``` + +Clears all retained records and resets `dropped_count()` to zero. It does not +close the sink; subsequent records begin a new history session. diff --git a/docs/api/history-sink-count.md b/docs/api/history-sink-count.md new file mode 100644 index 0000000..27e0f5d --- /dev/null +++ b/docs/api/history-sink-count.md @@ -0,0 +1,19 @@ +--- +name: history-sink-count +group: api +category: sink +update-time: 20260811 +description: Read the number of records currently retained by a HistorySink. +key-word: + - sink + - history + - count +--- + +## History-sink-count + +```moonbit +pub fn HistorySink::count(self : HistorySink) -> Int +``` + +Returns the current retained-record count, which never exceeds `capacity()`. diff --git a/docs/api/history-sink-dropped-count.md b/docs/api/history-sink-dropped-count.md new file mode 100644 index 0000000..bcc376e --- /dev/null +++ b/docs/api/history-sink-dropped-count.md @@ -0,0 +1,20 @@ +--- +name: history-sink-dropped-count +group: api +category: sink +update-time: 20260811 +description: Read the number of oldest records evicted from a HistorySink. +key-word: + - sink + - history + - dropped +--- + +## History-sink-dropped-count + +```moonbit +pub fn HistorySink::dropped_count(self : HistorySink) -> Int +``` + +Returns the cumulative number of records evicted because the bounded history was +full. `clear()` resets this count for the new history session. diff --git a/docs/api/history-sink-snapshot.md b/docs/api/history-sink-snapshot.md new file mode 100644 index 0000000..0938b59 --- /dev/null +++ b/docs/api/history-sink-snapshot.md @@ -0,0 +1,20 @@ +--- +name: history-sink-snapshot +group: api +category: sink +update-time: 20260811 +description: Copy the retained records from a HistorySink in chronological order. +key-word: + - sink + - history + - snapshot +--- + +## History-sink-snapshot + +```moonbit +pub fn HistorySink::snapshot(self : HistorySink) -> Array[Record] +``` + +Returns copied records ordered from oldest to newest. Later writes and mutation +of the returned array do not change the retained history. diff --git a/docs/api/history-sink-type.md b/docs/api/history-sink-type.md new file mode 100644 index 0000000..69fbb5d --- /dev/null +++ b/docs/api/history-sink-type.md @@ -0,0 +1,31 @@ +--- +name: history-sink-type +group: api +category: sink +update-time: 20260811 +description: Public bounded in-memory record sink used for diagnostic history. +key-word: + - sink + - history + - type +--- + +## History-sink-type + +`HistorySink` is the concrete synchronous sink returned by `history_sink(...)`. +It retains copied `Record` values in memory and implements `Sink`. + +### Interface + +```moonbit +pub struct HistorySink { + records : Ref[Array[Record]] + capacity : Int + dropped_count : Ref[Int] +} +``` + +### Notes + +Use `snapshot()` for retained records and the count helpers for bounded-history +observability. The type does not perform persistence or background work. diff --git a/docs/api/history-sink.md b/docs/api/history-sink.md new file mode 100644 index 0000000..1e743a5 --- /dev/null +++ b/docs/api/history-sink.md @@ -0,0 +1,45 @@ +--- +name: history-sink +group: api +category: sink +update-time: 20260811 +description: Create a bounded in-memory sink that retains the latest structured records. +key-word: + - sink + - history + - memory + - diagnostics +--- + +## History-sink + +Create a synchronous in-memory sink for bounded diagnostic history. It retains +the newest records and does not write to a file, console, or network endpoint. + +### Interface + +```moonbit +pub fn history_sink(capacity? : Int = 128) -> HistorySink +``` + +### Explanation + +- `capacity` is normalized to at least `1`; the default keeps the latest `128` + records. +- When full, the oldest record is evicted and `dropped_count()` increases. +- `snapshot()` returns records ordered from oldest to newest. `clear()` starts a + new in-memory history session. + +### How to Use + +```moonbit +let history = history_sink(capacity=64) +let logger = Logger::new(history, target="desktop") +logger.info("runtime ready") +let records = history.snapshot() +``` + +### Notes + +`HistorySink` is synchronous and application-owned. Serialize or persist a +snapshot explicitly when needed. diff --git a/docs/changes/1.2.0(0.8.0).md b/docs/changes/1.2.0(0.8.0).md new file mode 100644 index 0000000..7078685 --- /dev/null +++ b/docs/changes/1.2.0(0.8.0).md @@ -0,0 +1,15 @@ +## BitLogger Update Changes + +version 1.2.0(0.8.0) + +### Changes + +- Add a bounded in-memory HistorySink for retaining the latest structured records. + +### Verification + +- Run formatting, warning-denied checks and tests, API generation, documentation build, and packaged-output validation. + +### Notes + +- HistorySink is synchronous and memory-only; persistence and export remain application-owned. \ No newline at end of file diff --git a/docs/changes/index.md b/docs/changes/index.md index 55215d5..8ca6fa5 100644 --- a/docs/changes/index.md +++ b/docs/changes/index.md @@ -2,6 +2,7 @@ Versioned BitLogger change summaries. +- [1.2.0](./1.2.0(0.8.0).md) - [1.1.3](./1.1.3(0.7.3).md) - [1.1.2](./1.1.2(0.7.2).md) - [1.1.1](./1.1.1(0.7.1).md) diff --git a/docs/examples/console.md b/docs/examples/console.md index 04cf57b..28c164d 100644 --- a/docs/examples/console.md +++ b/docs/examples/console.md @@ -5,7 +5,7 @@ Use this flow for command-line tools and services that need a human-readable sta ## Install And Import ```bash -moon add Nanaloveyuki/BitLogger@0.7.3 +moon add Nanaloveyuki/BitLogger@0.8.0 ``` ```moonbit diff --git a/docs/zh/examples/console.md b/docs/zh/examples/console.md index 8088fe8..027a43e 100644 --- a/docs/zh/examples/console.md +++ b/docs/zh/examples/console.md @@ -7,7 +7,7 @@ ```bash moon new log-demo cd log-demo -moon add Nanaloveyuki/BitLogger@0.7.3 +moon add Nanaloveyuki/BitLogger@0.8.0 ``` 在应用的 `moon.pkg` 中加入: diff --git a/examples/basic/pkg.generated.mbti b/examples/basic/pkg.generated.mbti index 647a6d0..509ed44 100644 --- a/examples/basic/pkg.generated.mbti +++ b/examples/basic/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/basic" // Type aliases // Traits - diff --git a/examples/config_build/pkg.generated.mbti b/examples/config_build/pkg.generated.mbti index 18f7313..4e83f79 100644 --- a/examples/config_build/pkg.generated.mbti +++ b/examples/config_build/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/config_build" // Type aliases // Traits - diff --git a/examples/console_basic/pkg.generated.mbti b/examples/console_basic/pkg.generated.mbti index 89a6e03..131d8d4 100644 --- a/examples/console_basic/pkg.generated.mbti +++ b/examples/console_basic/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/console_basic" // Type aliases // Traits - diff --git a/examples/file_rotation/pkg.generated.mbti b/examples/file_rotation/pkg.generated.mbti index 7e7f57a..7a36c94 100644 --- a/examples/file_rotation/pkg.generated.mbti +++ b/examples/file_rotation/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/file_rotation" // Type aliases // Traits - diff --git a/examples/presets/pkg.generated.mbti b/examples/presets/pkg.generated.mbti index 7617b2c..f3ae23e 100644 --- a/examples/presets/pkg.generated.mbti +++ b/examples/presets/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/presets" // Type aliases // Traits - diff --git a/examples/style_tags/pkg.generated.mbti b/examples/style_tags/pkg.generated.mbti index ce8493c..9690899 100644 --- a/examples/style_tags/pkg.generated.mbti +++ b/examples/style_tags/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/style_tags" // Type aliases // Traits - diff --git a/examples/text_formatter/pkg.generated.mbti b/examples/text_formatter/pkg.generated.mbti index 7a18b3b..bca29dc 100644 --- a/examples/text_formatter/pkg.generated.mbti +++ b/examples/text_formatter/pkg.generated.mbti @@ -10,4 +10,3 @@ package "Nanaloveyuki/BitLogger/examples/text_formatter" // Type aliases // Traits - diff --git a/moon.mod b/moon.mod index e7f4bc4..8f77aed 100644 --- a/moon.mod +++ b/moon.mod @@ -1,6 +1,6 @@ name = "Nanaloveyuki/BitLogger" -version = "0.7.3" +version = "0.8.0" import { "moonbitlang/async@0.20.2", diff --git a/src-async/pkg.generated.mbti b/src-async/pkg.generated.mbti index 666a3b8..7d645fa 100644 --- a/src-async/pkg.generated.mbti +++ b/src-async/pkg.generated.mbti @@ -156,4 +156,3 @@ pub using @utils {type AsyncRuntimeMode} pub using @utils {type AsyncRuntimeState} // Traits - diff --git a/src-async/utils/pkg.generated.mbti b/src-async/utils/pkg.generated.mbti index 743ab58..a513e56 100644 --- a/src-async/utils/pkg.generated.mbti +++ b/src-async/utils/pkg.generated.mbti @@ -101,4 +101,3 @@ pub fn AsyncRuntimeState::new(AsyncRuntimeMode, Bool) -> Self // Type aliases // Traits - diff --git a/src/config_model/pkg.generated.mbti b/src/config_model/pkg.generated.mbti index 00eb9e9..ed1cc47 100644 --- a/src/config_model/pkg.generated.mbti +++ b/src/config_model/pkg.generated.mbti @@ -92,4 +92,3 @@ pub fn TextFormatterConfig::to_formatter(Self) -> @formatting.TextFormatter // Type aliases // Traits - diff --git a/src/core/pkg.generated.mbti b/src/core/pkg.generated.mbti index d5f9685..6ceec35 100644 --- a/src/core/pkg.generated.mbti +++ b/src/core/pkg.generated.mbti @@ -51,4 +51,3 @@ pub fn TraceContext::new(String, String, trace_flags? : String, trace_state? : S // Type aliases // Traits - diff --git a/src/file_model/pkg.generated.mbti b/src/file_model/pkg.generated.mbti index ce21120..9dca8cc 100644 --- a/src/file_model/pkg.generated.mbti +++ b/src/file_model/pkg.generated.mbti @@ -60,4 +60,3 @@ pub fn RuntimeFileState::new(FileSinkState, queued? : Bool, pending_count? : Int // Type aliases // Traits - diff --git a/src/file_runtime/pkg.generated.mbti b/src/file_runtime/pkg.generated.mbti index 69223fd..18654d5 100644 --- a/src/file_runtime/pkg.generated.mbti +++ b/src/file_runtime/pkg.generated.mbti @@ -50,4 +50,3 @@ pub impl @sink_graph.Sink for FileSink // Type aliases // Traits - diff --git a/src/formatting/pkg.generated.mbti b/src/formatting/pkg.generated.mbti index 29f9b88..5057865 100644 --- a/src/formatting/pkg.generated.mbti +++ b/src/formatting/pkg.generated.mbti @@ -93,4 +93,3 @@ pub struct TextStyle { pub type RecordFormatter = (@core.Record) -> String // Traits - diff --git a/src/pkg.generated.mbti b/src/pkg.generated.mbti index 986848d..90bb5a9 100644 --- a/src/pkg.generated.mbti +++ b/src/pkg.generated.mbti @@ -93,6 +93,8 @@ pub fn global_style_tag_registry() -> @formatting.StyleTagRegistry pub fn has_field(String) -> (@core.Record) -> Bool +pub fn history_sink(capacity? : Int) -> @sink_graph.HistorySink + pub fn identity_patch() -> (@core.Record) -> @core.Record pub fn info(String, fields? : Array[@core.Field]) -> Unit @@ -318,6 +320,8 @@ pub using @sink_graph {type FormattedCallbackSink} pub using @sink_graph {type FormattedConsoleSink} +pub using @sink_graph {type HistorySink} + pub using @sink_graph {type JsonConsoleSink} pub using @core {type Level} @@ -367,4 +371,3 @@ pub using @core {type TraceContext} pub using @sink_graph {trait Sink} // Traits - diff --git a/src/presets_pkg/pkg.generated.mbti b/src/presets_pkg/pkg.generated.mbti index e417204..933f2e6 100644 --- a/src/presets_pkg/pkg.generated.mbti +++ b/src/presets_pkg/pkg.generated.mbti @@ -30,4 +30,3 @@ pub fn with_queue(@config_model.LoggerConfig, max_pending? : Int, overflow? : @q // Type aliases // Traits - diff --git a/src/queue_model/pkg.generated.mbti b/src/queue_model/pkg.generated.mbti index d30138a..80506b4 100644 --- a/src/queue_model/pkg.generated.mbti +++ b/src/queue_model/pkg.generated.mbti @@ -14,4 +14,3 @@ pub(all) enum QueueOverflowPolicy { // Type aliases // Traits - diff --git a/src/record_ops/pkg.generated.mbti b/src/record_ops/pkg.generated.mbti index acc84e8..74183af 100644 --- a/src/record_ops/pkg.generated.mbti +++ b/src/record_ops/pkg.generated.mbti @@ -48,4 +48,3 @@ pub type RecordPatch = (@core.Record) -> @core.Record pub type RecordPredicate = (@core.Record) -> Bool // Traits - diff --git a/src/runtime/pkg.generated.mbti b/src/runtime/pkg.generated.mbti index a36e184..03c63e5 100644 --- a/src/runtime/pkg.generated.mbti +++ b/src/runtime/pkg.generated.mbti @@ -83,4 +83,3 @@ pub struct RuntimeSinkProgress { pub using @file_model {type RuntimeFileState} // Traits - diff --git a/src/sink_graph/pkg.generated.mbti b/src/sink_graph/pkg.generated.mbti index 5be94b8..88ce399 100644 --- a/src/sink_graph/pkg.generated.mbti +++ b/src/sink_graph/pkg.generated.mbti @@ -26,6 +26,8 @@ pub fn formatted_callback_sink((@core.Record) -> String, (String) -> Unit) -> Fo pub fn formatted_console_sink((@core.Record) -> String) -> FormattedConsoleSink +pub fn history_sink(capacity? : Int) -> HistorySink + pub fn json_console_sink() -> JsonConsoleSink pub fn[S] patch_sink(S, (@core.Record) -> @core.Record) -> PatchSink[S] @@ -91,6 +93,18 @@ pub struct FormattedConsoleSink { } pub impl Sink for FormattedConsoleSink +pub struct HistorySink { + records : @ref.Ref[Array[@core.Record]] + capacity : Int + dropped_count : @ref.Ref[Int] +} +pub fn HistorySink::capacity(Self) -> Int +pub fn HistorySink::clear(Self) -> Unit +pub fn HistorySink::count(Self) -> Int +pub fn HistorySink::dropped_count(Self) -> Int +pub fn HistorySink::snapshot(Self) -> Array[@core.Record] +pub impl Sink for HistorySink + pub struct JsonConsoleSink { _dummy : Unit } @@ -129,4 +143,3 @@ pub using @queue_model {type QueueOverflowPolicy} pub(open) trait Sink { fn write(Self, @core.Record) -> Unit } - diff --git a/src/sink_graph/sinks.mbt b/src/sink_graph/sinks.mbt index 4a3690d..de38f17 100644 --- a/src/sink_graph/sinks.mbt +++ b/src/sink_graph/sinks.mbt @@ -197,6 +197,69 @@ pub impl Sink for CallbackSink with fn write(self, rec) { (self.callback)(rec) } +///| +pub struct HistorySink { + records : Ref[Array[Record]] + capacity : Int + dropped_count : Ref[Int] +} + +///| +pub fn history_sink(capacity? : Int = 128) -> HistorySink { + let capacity = if capacity <= 0 { 1 } else { capacity } + { records: Ref([]), capacity, dropped_count: Ref(0) } +} + +///| +pub fn HistorySink::capacity(self : HistorySink) -> Int { + self.capacity +} + +///| +pub fn HistorySink::count(self : HistorySink) -> Int { + self.records.val.length() +} + +///| +pub fn HistorySink::dropped_count(self : HistorySink) -> Int { + self.dropped_count.val +} + +///| +fn copy_record(record : Record) -> Record { + Record::new( + record.level, + record.message, + timestamp_ms=record.timestamp_ms, + target=record.target, + fields=record.fields.map(field => Field::with_value(field, field.value)), + ) +} + +///| +pub fn HistorySink::snapshot(self : HistorySink) -> Array[Record] { + self.records.val.map(copy_record) +} + +///| +pub fn HistorySink::clear(self : HistorySink) -> Unit { + self.records.val = [] + self.dropped_count.val = 0 +} + +///| +pub impl Sink for HistorySink with fn write(self, rec) { + let records = self.records.val + if records.length() >= self.capacity { + self.dropped_count.val += 1 + let retained = records[1:].to_owned() + retained.push(copy_record(rec)) + self.records.val = retained + } else { + records.push(copy_record(rec)) + } +} + ///| pub struct BufferedSink[S] { sink : S diff --git a/src/sink_graph_surface_test.mbt b/src/sink_graph_surface_test.mbt index f3719f6..50175ef 100644 --- a/src/sink_graph_surface_test.mbt +++ b/src/sink_graph_surface_test.mbt @@ -15,6 +15,40 @@ test "callback sink receives record" { inspect(captured_message.val, content="hello") } +///| +test "history sink retains the newest copied records" { + let sink = history_sink(capacity=2) + let logger = Logger::new(sink, min_level=Level::Info, target="history") + logger.info("one", fields=[field("position", "first")]) + logger.info("two") + logger.info("three") + + inspect(sink.capacity(), content="2") + inspect(sink.count(), content="2") + inspect(sink.dropped_count(), content="1") + let snapshot = sink.snapshot() + inspect(snapshot.length(), content="2") + inspect(snapshot[0].message, content="two") + inspect(snapshot[1].message, content="three") + snapshot[0].fields.push(field("mutated", "outside")) + inspect(sink.snapshot()[0].fields.length(), content="0") +} + +///| +test "history sink normalizes capacity and clear resets its session" { + let sink = history_sink(capacity=0) + let logger = Logger::new(sink, min_level=Level::Info, target="history") + logger.info("one") + logger.info("two") + + inspect(sink.capacity(), content="1") + inspect(sink.count(), content="1") + inspect(sink.dropped_count(), content="1") + sink.clear() + inspect(sink.count(), content="0") + inspect(sink.dropped_count(), content="0") +} + ///| test "split sink routes records by predicate" { let left_messages : Ref[Array[String]] = Ref([]) diff --git a/src/sinks.mbt b/src/sinks.mbt index 71887a9..450db48 100644 --- a/src/sinks.mbt +++ b/src/sinks.mbt @@ -99,6 +99,14 @@ pub fn callback_sink(callback : (Record) -> Unit) -> CallbackSink { @sink_graph.callback_sink(callback) } +///| +pub type HistorySink = @sink_graph.HistorySink + +///| +pub fn history_sink(capacity? : Int = 128) -> HistorySink { + @sink_graph.history_sink(capacity~) +} + ///| pub type BufferedSink[S] = @sink_graph.BufferedSink[S] diff --git a/src/utils/pkg.generated.mbti b/src/utils/pkg.generated.mbti index 65754f0..8f117ec 100644 --- a/src/utils/pkg.generated.mbti +++ b/src/utils/pkg.generated.mbti @@ -110,4 +110,3 @@ pub using @formatting {type TextFormatter} pub using @formatting {type TextStyle} // Traits -