From 0f1342866f191f3888c7fef5e91bdd9b90e1f34a Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Sat, 31 Jan 2026 02:07:48 +0100 Subject: [PATCH 1/3] wip: more spinners --- src/simple_logging-spinners.ads | 19 +++++++++++++++++++ src/simple_logging.ads | 11 +++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/simple_logging-spinners.ads diff --git a/src/simple_logging-spinners.ads b/src/simple_logging-spinners.ads new file mode 100644 index 0000000..e95bfa2 --- /dev/null +++ b/src/simple_logging-spinners.ads @@ -0,0 +1,19 @@ +package Simple_Logging.Spinners is + + -- This package provides spinner definitions for use with the status line + -- functionality of Simple_Logging. + + -- ASCII safe + Classic : constant Any_Spinner := "/-\|"; + + -- Unicode spinners + Braille_6 : constant Any_Spinner := "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; + Braille_8 : constant Any_Spinner := "⡇⠇⠏⠋⠛⠙⠹⠸⢸⢰⣰⣠⣤⣄⣆⡆"; + Clocks : constant Any_Spinner := "🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛"; + Halves : constant Any_Spinner := "◐◓◑◒"; + Moon : constant Any_Spinner := "🌑🌒🌓🌔🌕🌖🌗🌘"; + Quarters : constant Any_Spinner := "◴◷◶◵"; + Squares : constant Any_Spinner := "◰◳◲◱"; + Triangles : constant Any_Spinner := "◢◣◤◥"; + +end Simple_Logging.Spinners; \ No newline at end of file diff --git a/src/simple_logging.ads b/src/simple_logging.ads index 1a02830..78d703e 100644 --- a/src/simple_logging.ads +++ b/src/simple_logging.ads @@ -99,6 +99,11 @@ package Simple_Logging with Preelaborate is Location : String := Gnat.Source_Info.Source_Location) is null; -- Quietly drop + type Any_Spinner is new Wide_Wide_String; + -- Sequence of chars to loop through for spinner animation + + function Default_Spinner return Any_Spinner; + ----------------- -- Status line -- ----------------- @@ -111,14 +116,16 @@ package Simple_Logging with Preelaborate is function Activity (Text : String; Autocomplete_Text : String := ""; - Level : Levels := Info) return Ongoing; + Spinner : Any_Spinner := Default_Spinner; + Level : Levels := Info) + return Ongoing; -- Start an ongoing activity with given Text. If Autocomplete_Text is -- provided, it will be used to complete the text when the activity ends. -- When ASCII_Only is True, this results in "Done: " -- being printed; otherwise, a checkmark-prefixed message is printed. -- In both cases the status line is cleared. You can also use New_Line to -- print a custom message and to jump to the next line, at end or - -- mid-progress. + -- mid-progress. See the Spinners child package for predefined spinners. procedure Step (This : in out Ongoing; New_Text : String := ""; From f220992a49a652f00d86fb0cd92332d96d179e42 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Sat, 31 Jan 2026 11:19:57 +0100 Subject: [PATCH 2/3] rc1 --- src/simple_logging-spinners.ads | 4 +- src/simple_logging.adb | 80 +++++++++++++++------------------ src/simple_logging.ads | 23 +++++++--- 3 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/simple_logging-spinners.ads b/src/simple_logging-spinners.ads index e95bfa2..36ca0da 100644 --- a/src/simple_logging-spinners.ads +++ b/src/simple_logging-spinners.ads @@ -1,10 +1,10 @@ -package Simple_Logging.Spinners is +package Simple_Logging.Spinners with Preelaborate is -- This package provides spinner definitions for use with the status line -- functionality of Simple_Logging. -- ASCII safe - Classic : constant Any_Spinner := "/-\|"; + Classic : constant Any_Spinner := "/-\|"; -- Unicode spinners Braille_6 : constant Any_Spinner := "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; diff --git a/src/simple_logging.adb b/src/simple_logging.adb index fa39466..bbbb460 100644 --- a/src/simple_logging.adb +++ b/src/simple_logging.adb @@ -5,6 +5,7 @@ with GNAT.IO; with Simple_Logging.C; with Simple_Logging.Decorators; with Simple_Logging.Filtering; +with Simple_Logging.Spinners; with Simple_Logging.Support; pragma Warnings (Off); @@ -128,25 +129,8 @@ package body Simple_Logging is Statuses : Status_Sets.Set; - subtype Indicator_Range is Positive range 1 .. 4; - Indicator_Nice : constant array (Indicator_Range) of Wide_Wide_Character := - ('◴', - '◷', - '◶', - '◵'); - Indicator_Basic : constant array (Indicator_Range) of String (1 .. 1) := - ("/", - "-", - "\", - "|"); - - Ind_Pos : Positive := 1; - Last_Step : Duration := 0.0; - - function Indicator return String is - (if Is_TTY and then not ASCII_Only - then U ("" & Indicator_Nice (Ind_Pos)) - else Indicator_Basic (Ind_Pos)); + Last_Status_Line : Unbounded_String; -- Used for cleanup + Last_Spin : Duration := 0.0; -------------- -- Activity -- @@ -154,15 +138,23 @@ package body Simple_Logging is function Activity (Text : String; Autocomplete_Text : String := ""; - Level : Levels := Info) return Ongoing is + Spinner : Any_Spinner := Default_Spinner; + Level : Levels := Info) + return Ongoing + is begin return This : Ongoing := (Ada.Finalization.Limited_Controlled with Data => (Level => Level, Start => Internal_Clock, - Text => To_Unbounded_String (Text), - Text_Autocomplete => - To_Unbounded_String (Autocomplete_Text))) + Text => To_Unbounded_String (Text)), + Text_Autocomplete => + To_Unbounded_String (Autocomplete_Text), + Spinner => Spinner_Holders.To_Holder + (if Spinner = Default_Spinner + then (if ASCII_Only then Spinners.Classic else Spinners.Braille_8) + else Spinner), + Spinner_Pos => <>) do Debug ("Status start: " & To_String (This.Data.Text)); Statuses.Insert (This.Data); @@ -174,11 +166,20 @@ package body Simple_Logging is -- Build_Status_Line -- ----------------------- - function Build_Status_Line return String is + function Build_Status_Line (This : in out Ongoing) return String is Line : Unbounded_String; Pred : Unbounded_String; -- Status of the precedent scope, to eliminate duplicates begin + if Internal_Clock - Last_Spin > Spinner_Period then + This.Spinner_Pos := This.Spinner_Pos + 1; + Last_Spin := Internal_Clock; + + if This.Spinner_Pos not in This.Spinner.Reference.Element'Range then + This.Spinner_Pos := This.Spinner.Reference.Element'First; + end if; + end if; + for Status of Statuses loop if Status.Level <= Simple_Logging.Level and then Pred /= Status.Text @@ -190,7 +191,9 @@ package body Simple_Logging is end loop; if Length (Line) > 0 then - Line := Indicator & " " & Line; + Line := + U ("" & This.Spinner.Reference.Element (This.Spinner_Pos)) + & " " & Line; end if; return To_String (Line); @@ -204,7 +207,7 @@ package body Simple_Logging is Line : constant String := (if Old_Status /= "" then Old_Status - else Build_Status_Line); + else To_String (Last_Status_Line)); begin if Is_TTY and then Visible_Length (Line) > 0 then GNAT.IO.Put @@ -220,8 +223,8 @@ package body Simple_Logging is procedure Finalize (This : in out Ongoing) is begin Debug ("Status ended: " & To_String (This.Data.Text)); - if this.Data.Text_Autocomplete /= "" then - this.New_Line (To_String (This.Data.Text_Autocomplete)); + if this.Text_Autocomplete /= "" then + this.New_Line (To_String (This.Text_Autocomplete)); else Clear_Status_Line; Statuses.Difference (Status_Sets.To_Set (This.Data)); @@ -236,7 +239,7 @@ package body Simple_Logging is procedure New_Line (This : in out Ongoing; Text : String) is - Old_Line : constant String := Build_Status_Line; + Old_Line : constant String := This.Build_Status_Line; begin -- Remove current status (unsure if this is needed) Statuses.Exclude (This.Data); @@ -270,7 +273,7 @@ package body Simple_Logging is procedure Step (This : in out Ongoing; New_Text : String := ""; Clear : Boolean := False) is - Old_Line : constant String := Build_Status_Line; + Old_Line : constant String := This.Build_Status_Line; begin -- Update status if needed if New_Text /= "" or else Clear then @@ -280,27 +283,18 @@ package body Simple_Logging is end if; declare - New_Line : constant String := Build_Status_Line; + New_Line : constant String := This.Build_Status_Line; New_Len : constant Natural := Visible_Length (New_Line); Old_Len : constant Natural := Visible_Length (Old_Line); begin + -- Store for future reference + Last_Status_Line := To_Unbounded_String (New_Line); + if Is_TTY and then New_Len > 0 then GNAT.IO.Put (ASCII.CR & New_Line & (1 .. Old_Len - Natural'Min (New_Len, Old_Len) => ' ')); C.Flush_Stdout; - - -- Advance the spinner - - if Last_Step = 0.0 or else - Internal_Clock - Last_Step >= Spinner_Period - then - Last_Step := Internal_Clock; - Ind_Pos := Ind_Pos + 1; - if Ind_Pos > Indicator_Range'Last then - Ind_Pos := Indicator_Range'First; - end if; - end if; else Clear_Status_Line (Old_Line); end if; diff --git a/src/simple_logging.ads b/src/simple_logging.ads index 78d703e..d2afa5c 100644 --- a/src/simple_logging.ads +++ b/src/simple_logging.ads @@ -1,5 +1,6 @@ with GNAT.Source_Info; +private with Ada.Containers.Indefinite_Holders; private with Ada.Finalization; private with Ada.Strings.Unbounded; with Ada.Strings.UTF_Encoding.Wide_Wide_Strings; @@ -64,7 +65,8 @@ package Simple_Logging with Preelaborate is -- When True, Stdout_Level also applies to the Always level Spinner_Period : Duration := 0.1; - -- Time between spinner frame changes + -- Time between spinner frame changes. TODO: make this a property of the + -- spinner itself. procedure Log (Message : String; Level : Levels := Info; @@ -157,31 +159,40 @@ private use Ada.Strings.Unbounded; + package Spinner_Holders is new + Ada.Containers.Indefinite_Holders (Any_Spinner); + type Ongoing_Data is record Start : Duration; Level : Levels; Text : Unbounded_String; - Text_Autocomplete : Unbounded_String; end record; -- Non-limited data to be stored in collections type Ongoing is new Ada.Finalization.Limited_Controlled with record Data : Ongoing_Data; + + -- Rest of state not needed to rebuild the status line + Text_Autocomplete : Unbounded_String; + Spinner : Spinner_Holders.Holder; + Spinner_Pos : Integer := Integer'First; end record; + -- Note: we consider only a single spinner active so their status is shared + -- by means of global variables in the body. function "<" (L, R : Ongoing_Data) return Boolean is (L.Start < R.Start or else (L.Start = R.Start and then L.Level < R.Level) or else - (L.Start = R.Start and then L.Level = R.Level and then L.Text < R.Text) or else - (L.Start = R.Start and then L.Level = R.Level and then L.Text = R.Text - and then L.Text_Autocomplete < R.Text_Autocomplete)); + (L.Start = R.Start and then L.Level = R.Level and then L.Text < R.Text)); overriding procedure Finalize (This : in out Ongoing); - function Build_Status_Line return String; + function Build_Status_Line (This : in out Ongoing) return String; procedure Clear_Status_Line (Old_Status : String := ""); -- Use the old status if provided, or the current one otherwise + function Default_Spinner return Any_Spinner is (""); + end Simple_Logging; From 9d863fab3ce846e796e926665b9670bdaeb08660 Mon Sep 17 00:00:00 2001 From: "Alejandro R. Mosteo" Date: Sat, 31 Jan 2026 12:05:30 +0100 Subject: [PATCH 3/3] self-review --- src/simple_logging.adb | 48 +++++++++++++++++++++++++++++++----------- src/simple_logging.ads | 18 +++++++--------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/simple_logging.adb b/src/simple_logging.adb index bbbb460..c3b251e 100644 --- a/src/simple_logging.adb +++ b/src/simple_logging.adb @@ -131,6 +131,27 @@ package body Simple_Logging is Last_Status_Line : Unbounded_String; -- Used for cleanup Last_Spin : Duration := 0.0; + Spinner : Spinner_Holders.Holder; + Spinner_Pos : Integer := 0; + + ----------------- + -- Set_Spinner -- + ----------------- + + procedure Set_Spinner (Spinner : Any_Spinner) is + begin + if ASCII_Only and then (for some Char of Spinner => + Wide_Wide_Character'Pos (Char) > 127) + then + Simple_Logging.Spinner.Replace_Element (Spinners.Classic); + Warning ("Using default spinner as requested one is not ASCII-only"); + elsif Spinner'Length = 0 then + Simple_Logging.Spinner.Replace_Element (Spinners.Classic); + Warning ("Using default spinner as requested one is empty"); + else + Simple_Logging.Spinner.Replace_Element (Spinner); + end if; + end Set_Spinner; -------------- -- Activity -- @@ -138,7 +159,6 @@ package body Simple_Logging is function Activity (Text : String; Autocomplete_Text : String := ""; - Spinner : Any_Spinner := Default_Spinner; Level : Levels := Info) return Ongoing is @@ -149,12 +169,7 @@ package body Simple_Logging is Start => Internal_Clock, Text => To_Unbounded_String (Text)), Text_Autocomplete => - To_Unbounded_String (Autocomplete_Text), - Spinner => Spinner_Holders.To_Holder - (if Spinner = Default_Spinner - then (if ASCII_Only then Spinners.Classic else Spinners.Braille_8) - else Spinner), - Spinner_Pos => <>) + To_Unbounded_String (Autocomplete_Text)) do Debug ("Status start: " & To_String (This.Data.Text)); Statuses.Insert (This.Data); @@ -167,17 +182,26 @@ package body Simple_Logging is ----------------------- function Build_Status_Line (This : in out Ongoing) return String is + pragma Unreferenced (This); + -- Not used right now but likely to be necessary in the future + Line : Unbounded_String; Pred : Unbounded_String; -- Status of the precedent scope, to eliminate duplicates begin if Internal_Clock - Last_Spin > Spinner_Period then - This.Spinner_Pos := This.Spinner_Pos + 1; + Spinner_Pos := Spinner_Pos + 1; Last_Spin := Internal_Clock; + end if; - if This.Spinner_Pos not in This.Spinner.Reference.Element'Range then - This.Spinner_Pos := This.Spinner.Reference.Element'First; - end if; + -- Ensure there is a spinner configured (cannot be done before due to + -- pre-elaboration). + if Spinner.Is_Empty then + Spinner.Replace_Element (Spinners.Classic); + end if; + + if Spinner_Pos not in Spinner.Reference.Element'Range then + Spinner_Pos := Spinner.Reference.Element'First; end if; for Status of Statuses loop @@ -192,7 +216,7 @@ package body Simple_Logging is if Length (Line) > 0 then Line := - U ("" & This.Spinner.Reference.Element (This.Spinner_Pos)) + U ("" & Spinner.Reference.Element (Spinner_Pos)) & " " & Line; end if; diff --git a/src/simple_logging.ads b/src/simple_logging.ads index d2afa5c..9b11066 100644 --- a/src/simple_logging.ads +++ b/src/simple_logging.ads @@ -104,12 +104,15 @@ package Simple_Logging with Preelaborate is type Any_Spinner is new Wide_Wide_String; -- Sequence of chars to loop through for spinner animation - function Default_Spinner return Any_Spinner; - ----------------- -- Status line -- ----------------- + procedure Set_Spinner (Spinner : Any_Spinner); + -- Set the global spinner model to use. The default spinner will be forced + -- if ASCII_Only is True. See the Spinners child package for predefined + -- spinners. + type Ongoing (<>) is tagged limited private; -- The status line is used to present an ongoing activity. This is done -- through a scoped type. Several nested statuses can be created, and the @@ -118,7 +121,6 @@ package Simple_Logging with Preelaborate is function Activity (Text : String; Autocomplete_Text : String := ""; - Spinner : Any_Spinner := Default_Spinner; Level : Levels := Info) return Ongoing; -- Start an ongoing activity with given Text. If Autocomplete_Text is @@ -127,7 +129,7 @@ package Simple_Logging with Preelaborate is -- being printed; otherwise, a checkmark-prefixed message is printed. -- In both cases the status line is cleared. You can also use New_Line to -- print a custom message and to jump to the next line, at end or - -- mid-progress. See the Spinners child package for predefined spinners. + -- mid-progress. procedure Step (This : in out Ongoing; New_Text : String := ""; @@ -174,11 +176,9 @@ private -- Rest of state not needed to rebuild the status line Text_Autocomplete : Unbounded_String; - Spinner : Spinner_Holders.Holder; - Spinner_Pos : Integer := Integer'First; end record; - -- Note: we consider only a single spinner active so their status is shared - -- by means of global variables in the body. + -- Note: Although activities can be nested, there is only a global spinner + -- so all that state is in the body. function "<" (L, R : Ongoing_Data) return Boolean is (L.Start < R.Start or else @@ -193,6 +193,4 @@ private procedure Clear_Status_Line (Old_Status : String := ""); -- Use the old status if provided, or the current one otherwise - function Default_Spinner return Any_Spinner is (""); - end Simple_Logging;