Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/simple_logging-spinners.ads
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 := "/-\|";

-- 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;
104 changes: 61 additions & 43 deletions src/simple_logging.adb
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -128,41 +129,47 @@ 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;
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 --
--------------

function Activity (Text : String;
Autocomplete_Text : String := "";
Level : Levels := Info) return Ongoing is
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))
do
Debug ("Status start: " & To_String (This.Data.Text));
Statuses.Insert (This.Data);
Expand All @@ -174,11 +181,29 @@ 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
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
Spinner_Pos := Spinner_Pos + 1;
Last_Spin := Internal_Clock;
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
if Status.Level <= Simple_Logging.Level
and then Pred /= Status.Text
Expand All @@ -190,7 +215,9 @@ package body Simple_Logging is
end loop;

if Length (Line) > 0 then
Line := Indicator & " " & Line;
Line :=
U ("" & Spinner.Reference.Element (Spinner_Pos))
& " " & Line;
end if;

return To_String (Line);
Expand All @@ -204,7 +231,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
Expand All @@ -220,8 +247,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));
Expand All @@ -236,7 +263,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);
Expand Down Expand Up @@ -270,7 +297,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
Expand All @@ -280,27 +307,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;
Expand Down
30 changes: 23 additions & 7 deletions src/simple_logging.ads
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -99,10 +101,18 @@ 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

-----------------
-- 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
Expand All @@ -111,7 +121,8 @@ package Simple_Logging with Preelaborate is

function Activity (Text : String;
Autocomplete_Text : String := "";
Level : Levels := Info) return Ongoing;
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: <Autocomplete_Text>"
Expand Down Expand Up @@ -150,29 +161,34 @@ 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;
end record;
-- 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
(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
Expand Down