1
- """Rich logging configuration for Tux.
1
+ """
2
+ Rich logging configuration for Tux.
2
3
3
4
This module sets up global logging configuration using loguru with Rich formatting.
4
5
It should be imported and initialized at the start of the application.
24
25
def highlight (style : str ) -> dict [str , Callable [[Text ], Text ]]:
25
26
"""
26
27
Create a highlighter function for the given style.
27
-
28
- Parameters
29
- ----------
30
- style : str
31
- The style to apply to the text
32
-
33
- Returns
34
- -------
35
- dict[str, Callable[[Text], Text]]
36
- A dict containing the highlighter function
37
28
"""
38
29
39
30
def highlighter (text : Text ) -> Text :
@@ -49,96 +40,139 @@ class RichHandlerProtocol(Protocol):
49
40
50
41
def __init__ (self , * args : Any , ** kwargs : Any ) -> None : ...
51
42
52
- """
53
- Initialize the Rich handler.
54
-
55
- Parameters
56
- ----------
57
- *args : Any
58
- The arguments to pass to the RichHandler constructor
59
- **kwargs : Any
60
- The keyword arguments to pass to the RichHandler constructor
61
- """
62
-
63
43
def render_message (self , record : LogRecord , message : str ) -> ConsoleRenderable : ...
64
44
65
45
66
46
class LoguruRichHandler (RichHandler , RichHandlerProtocol ):
67
47
"""
68
- Enhanced Rich handler for loguru that supports better styling and formatting.
48
+ Enhanced Rich handler for loguru that splits long messages into two lines.
49
+
50
+ For messages that fit within the available space (i.e. between the prefix
51
+ and the right-aligned source info), a single line is printed. If the
52
+ message is too long, then:
53
+
54
+ - The first line prints as much of the message as possible.
55
+ - The second line starts with a continued prefix that is spaced to match
56
+ the normal prefix and prints the remainder (with the source info right-aligned).
57
+
58
+ The normal prefix is:
59
+
60
+ █ [HH:MM:SS][LEVEL ]
61
+
62
+ and the continued prefix is:
63
+
64
+ █ [CONTINUED ]
69
65
"""
70
66
71
67
def __init__ (self , * args : Any , ** kwargs : Any ) -> None :
72
- """
73
- Initialize the Rich handler.
74
-
75
- Parameters
76
- ----------
77
- *args : Any
78
- The arguments to pass to the RichHandler constructor
79
- **kwargs : Any
80
- The keyword arguments to pass to the RichHandler constructor
81
- """
82
68
super ().__init__ (* args , ** kwargs )
83
69
self ._last_time : Text | None = None
84
70
85
71
def emit (self , record : LogRecord ) -> None :
86
- """
87
- Emit a log record.
88
-
89
- Parameters
90
- ----------
91
- record : LogRecord
92
- The log record to emit.
93
- """
94
72
try :
95
- # Get the formatted message
73
+ # Get the formatted message from loguru's formatter.
96
74
message = self .format (record )
97
75
98
- # Handle time formatting
76
+ # --- Time formatting ---
99
77
time_format : str | Callable [[datetime ], Text ] | None = (
100
78
None if self .formatter is None else self .formatter .datefmt
101
79
)
102
80
time_format = time_format or self ._log_render .time_format
103
81
log_time = datetime .fromtimestamp (record .created , tz = UTC )
104
-
105
- # Handle callable time format
106
82
if callable (time_format ):
107
83
log_time_str = str (time_format (log_time ))
108
84
else :
109
85
log_time_str = log_time .strftime (time_format or "[%X]" )
110
86
111
- # Format the level with symbols
87
+ # --- Level symbol and text ---
112
88
level_name = record .levelname .lower ()
113
89
level_symbols = {
114
90
"debug" : "[bold cyan]█[/]" , # Cyan block for debug
115
91
"info" : "[bold blue]█[/]" , # Blue block for info
116
92
"warning" : "[bold yellow]█[/]" , # Yellow block for warning
117
93
"error" : "[bold red]█[/]" , # Red block for error
118
- "critical" : "[bold red on red]█[/]" , # Red block on red bg for critical
94
+ "critical" : "[bold red on red]█[/]" , # Red block on red background for critical
119
95
"success" : "[bold green]█[/]" , # Green block for success
120
96
"trace" : "[dim]█[/]" , # Dim block for trace
121
97
}
122
- symbol = level_symbols .get (level_name , "[bright_black]█[/]" ) # Gray block for default
123
- level_str = f"{ record .levelname :<7} " # Reduced padding by 1
124
-
125
- # Format source info and display it as part of the log prefix (before the actual message)
126
- source_info = (
127
- f"[dim]{ record .funcName } [bright_black] @ [/bright_black]{ record .filename } :{ record .lineno } [/dim]"
98
+ symbol = level_symbols .get (level_name , "[bright_black]█[/]" ) # Default gray block
99
+
100
+ # --- Constants ---
101
+ level_field_width = 10 # Adjust as needed
102
+
103
+ # --- Build the normal prefix ---
104
+ # Example: "█ [02:06:55][INFO ]"
105
+ first_prefix_markup = (
106
+ f"{ symbol } "
107
+ + f"[log.time]{ log_time_str } [/]"
108
+ + "[log.bracket][[/]"
109
+ + f"[logging.level.{ level_name } ]{ record .levelname .upper ().ljust (level_field_width )} [/]"
110
+ + "[log.bracket]][/]"
111
+ + " "
128
112
)
129
-
130
- log_prefix = (
131
- f"{ symbol } [log.time]{ log_time_str } [/]"
132
- f"[log.bracket][[/][logging.level.{ level_name } ]{ level_str } [/][log.bracket]][/] "
133
- f"{ source_info } "
113
+ first_prefix_plain = Text .from_markup (first_prefix_markup ).plain
114
+
115
+ # --- Build the continued prefix ---
116
+ # We want the continued prefix to have the same plain-text width as the normal one.
117
+ # The normal prefix width (plain) is:
118
+ # len(symbol + " ") + (len(log_time_str) + 2) + (LEVEL_FIELD_WIDTH + 2)
119
+ # For the continued prefix we print "CONTINUED" (padded) in a single bracket:
120
+ # Total width = len(symbol + " ") + (continued_field_width + 2)
121
+ # Setting these equal gives:
122
+ # continued_field_width = len(log_time_str) + LEVEL_FIELD_WIDTH + 2
123
+ continued_field_width = len (log_time_str ) + level_field_width
124
+ continued_prefix_markup = (
125
+ f"{ symbol } "
126
+ + "[log.bracket][[/]"
127
+ + f"[logging.level.info]{ 'CONTINUED' .ljust (continued_field_width )} [/]"
128
+ + "[log.bracket]][/]"
129
+ + " "
134
130
)
131
+ continued_prefix_plain = Text .from_markup (continued_prefix_markup ).plain
135
132
136
- # Print the complete log line with the source info preceding the actual log message.
137
- self .console .print (
138
- f"{ log_prefix } { message } " ,
139
- markup = True ,
140
- highlight = False ,
133
+ # --- Source info ---
134
+ # For example: "run @ main.py:215"
135
+ source_info = (
136
+ f"[dim]{ record .funcName } [bright_black] @ [/bright_black]{ record .filename } :{ record .lineno } [/dim]"
141
137
)
138
+ source_info_plain = Text .from_markup (source_info ).plain
139
+
140
+ # --- Total width ---
141
+ # Use the console's actual width if available.
142
+ total_width = (self .console .size .width or self .console .width ) or 80
143
+
144
+ # Convert the formatted message to plain text.
145
+ plain_message = Text .from_markup (message ).plain
146
+
147
+ # --- One-line vs two-line decision ---
148
+ # For one-line messages, the available space is the total width
149
+ # minus the widths of the normal prefix and the source info.
150
+ available_for_message = total_width - len (first_prefix_plain ) - len (source_info_plain )
151
+ if len (plain_message ) <= available_for_message :
152
+ # The message fits on one line.
153
+ padded_msg = plain_message .ljust (available_for_message )
154
+ full_line = first_prefix_markup + padded_msg + source_info
155
+ self .console .print (full_line , markup = True , highlight = False )
156
+ else :
157
+ # --- Two-line (continued) layout ---
158
+ # First line: Reserve all space after the normal prefix.
159
+ first_line_area = total_width - len (first_prefix_plain )
160
+ first_line_msg = plain_message [:first_line_area ] # Simply cut off without ellipsis
161
+
162
+ # Second line: use the continued prefix and reserve space for the source info.
163
+ second_line_area = total_width - len (continued_prefix_plain ) - len (source_info_plain )
164
+ # The remainder of the message is everything after what was printed on the first line.
165
+ remainder_start = first_line_area # Adjusted to not account for ellipsis
166
+ second_line_msg = plain_message [remainder_start :]
167
+ if len (second_line_msg ) > second_line_area :
168
+ second_line_msg = second_line_msg [:second_line_area ] # Simply cut off without ellipsis
169
+ padded_second_line_msg = second_line_msg .ljust (second_line_area )
170
+ self .console .print (first_prefix_markup + first_line_msg , markup = True , highlight = False )
171
+ self .console .print (
172
+ continued_prefix_markup + padded_second_line_msg + source_info ,
173
+ markup = True ,
174
+ highlight = False ,
175
+ )
142
176
except Exception :
143
177
self .handleError (record )
144
178
@@ -163,21 +197,20 @@ def setup_logging() -> None:
163
197
),
164
198
)
165
199
166
- # Configure loguru with Rich handler
167
200
logger .configure (
168
201
handlers = [
169
202
{
170
203
"sink" : LoguruRichHandler (
171
204
console = console ,
172
- show_time = False , # We handle time display ourselves
205
+ show_time = False , # We display time ourselves.
173
206
show_path = False ,
174
207
rich_tracebacks = True ,
175
208
tracebacks_show_locals = True ,
176
209
log_time_format = "[%X]" ,
177
210
markup = True ,
178
- highlighter = None , # Disable automatic highlighting
211
+ highlighter = None ,
179
212
),
180
- "format" : "{message}" , # Just the message since we handle the rest
213
+ "format" : "{message}" ,
181
214
"level" : "DEBUG" if Config .DEV else "INFO" ,
182
215
},
183
216
],
0 commit comments