github / src / accesspanel / extensions / ansi.py @ dc5fb9be
1 |
# Copyright (c) 2016, LE GOFF Vincent |
---|---|
2 |
# All rights reserved.
|
3 |
|
4 |
# Redistribution and use in source and binary forms, with or without
|
5 |
# modification, are permitted provided that the following conditions are met:
|
6 |
|
7 |
# * Redistributions of source code must retain the above copyright notice, this
|
8 |
# list of conditions and the following disclaimer.
|
9 |
|
10 |
# * Redistributions in binary form must reproduce the above copyright notice,
|
11 |
# this list of conditions and the following disclaimer in the documentation
|
12 |
# and/or other materials provided with the distribution.
|
13 |
|
14 |
# * Neither the name of ytranslate nor the names of its
|
15 |
# contributors may be used to endorse or promote products derived from
|
16 |
# this software without specific prior written permission.
|
17 |
|
18 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
21 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
22 |
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
24 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
25 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
26 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
27 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28 |
|
29 |
import re, string |
30 |
|
31 |
import wx |
32 |
import wx.lib.colourdb |
33 |
|
34 |
from .base import BaseExtension |
35 |
|
36 |
## Constants
|
37 |
# Regular expressions to capture ANSI codes
|
38 |
RE_CODE = re.compile(r"^(?:(\d+)(?:\;(\d+)(?:\;(\d+))?)?)?$", re.UNICODE)
|
39 |
|
40 |
# Brightness
|
41 |
NORMAL = 1
|
42 |
BRIGHT = 2
|
43 |
UNDERLINE = 3
|
44 |
|
45 |
class ANSI(BaseExtension): |
46 |
|
47 |
"""Manage the ANSI codes to have color in the text.
|
48 |
|
49 |
The AccessPanel can support color codes. This extension allows
|
50 |
to write ANSI color codes and put the text in correct formatting.
|
51 |
|
52 |
Brief reminder:
|
53 |
ANSI codes are placed in the text, between a '\x1b[' sequence
|
54 |
and a final 'm'. In between are numbers separated with a
|
55 |
semicolon. The first number represents the level of
|
56 |
brightness (0 for normal, 1 for bright, 4 for underline,
|
57 |
7 for negative). The second number represents the foreground
|
58 |
color and should be between 30 and 37 (see the list below).
|
59 |
The third number represents the background color and should be
|
60 |
between 40 and 47 (the same list of color applies).
|
61 |
|
62 |
List of colors:
|
63 |
| Color | Foreground | Background |
|
64 |
| Black | 30 | 40 |
|
65 |
| Red | 31 | 41 |
|
66 |
| Green | 32 | 42 |
|
67 |
| Yellow | 33 | 43 |
|
68 |
| Blue | 34 | 44 |
|
69 |
| Magenta | 35 | 45 |
|
70 |
| Cyan | 36 | 46 |
|
71 |
| White | 37 | 47 |
|
72 |
|
73 |
Examples:
|
74 |
"\x1b[0;31;47m" means red on white.
|
75 |
"\x1b[1;33m" means bright yellow on default background.
|
76 |
"\x1b[4;36m" means underline cyan on default background.
|
77 |
"\x1b[0m" means back to default colors.
|
78 |
|
79 |
"""
|
80 |
|
81 |
def __init__(self, panel): |
82 |
super().__init__(panel)
|
83 |
wx.lib.colourdb.updateColourDB() |
84 |
self.brightness = NORMAL
|
85 |
self.foreground = wx.BLACK
|
86 |
self.background = wx.WHITE
|
87 |
self.default_foreground = wx.BLACK
|
88 |
self.default_background = wx.WHITE
|
89 |
self.modifiers = []
|
90 |
|
91 |
# Color codes
|
92 |
self.normal_colors = {
|
93 |
40: wx.NamedColour("dark grey"), |
94 |
41: wx.RED,
|
95 |
42: wx.GREEN,
|
96 |
43: wx.YELLOW,
|
97 |
44: wx.BLUE,
|
98 |
45: wx.NamedColour("magenta"), |
99 |
46: wx.CYAN,
|
100 |
47: wx.WHITE,
|
101 |
} |
102 |
|
103 |
self.bright_colors = {
|
104 |
40: wx.NamedColour("grey"), |
105 |
41: wx.NamedColour("deep pink"), |
106 |
42: wx.NamedColour("light green"), |
107 |
43: wx.NamedColour("light yellow"), |
108 |
44: wx.NamedColour("light blue"), |
109 |
45: wx.NamedColour("light magenta"), |
110 |
46: wx.NamedColour("light cyan"), |
111 |
47: wx.WHITE,
|
112 |
} |
113 |
|
114 |
self.dark_colors = {
|
115 |
40: wx.BLACK,
|
116 |
41: wx.NamedColour("dark red"), |
117 |
42: wx.NamedColour("dark green"), |
118 |
43: wx.NamedColour("orange"), |
119 |
44: wx.NamedColour("dark blue"), |
120 |
45: wx.NamedColour("dark magenta"), |
121 |
46: wx.NamedColour("dark cyan"), |
122 |
47: wx.WHITE,
|
123 |
} |
124 |
|
125 |
def OnClearOutput(self): |
126 |
"""The output has been cleared."""
|
127 |
self.modifiers = []
|
128 |
|
129 |
def OnMessage(self, message): |
130 |
"""Interpret the ANSI codes."""
|
131 |
|
132 |
# Variables
|
133 |
point = self.panel.editing_pos
|
134 |
ansi_stage = 1
|
135 |
ansi_buffer = ""
|
136 |
clean_buffer = ""
|
137 |
char_index = 0
|
138 |
|
139 |
def select_colors(code): |
140 |
"""
|
141 |
Transforms ANSI sequences in a format specifier
|
142 |
"""
|
143 |
|
144 |
match = RE_CODE.match(code) |
145 |
|
146 |
# ANSI style sequences have tree parts
|
147 |
p1, p2, p3 = match.groups() |
148 |
brightness, foreground, background = (None, None, None) |
149 |
|
150 |
if p1:
|
151 |
p1 = int(p1.strip())
|
152 |
|
153 |
if p1 in (0, 1, 4, 7): |
154 |
brightness = p1 |
155 |
elif p1 in range(30, 38): |
156 |
foreground = p1 |
157 |
elif p1 in range(40, 48): |
158 |
background = p1 |
159 |
|
160 |
if p2:
|
161 |
p2 = int(p2.strip())
|
162 |
|
163 |
if p2 in range(30, 40): |
164 |
foreground = p2 |
165 |
elif p2 in range(40, 50): |
166 |
background = p2 |
167 |
|
168 |
if p3:
|
169 |
p3 = int(p3.strip())
|
170 |
|
171 |
if p3 in range(40, 50): |
172 |
background = p3 |
173 |
|
174 |
if brightness == 1: |
175 |
brightness = BRIGHT |
176 |
elif brightness == 4: |
177 |
brightness = UNDERLINE |
178 |
else:
|
179 |
brightness = NORMAL |
180 |
|
181 |
|
182 |
# Now the colors
|
183 |
colorlist = None
|
184 |
|
185 |
if brightness == BRIGHT:
|
186 |
colorlist = self.bright_colors
|
187 |
elif brightness == UNDERLINE:
|
188 |
colorlist = self.dark_colors
|
189 |
else:
|
190 |
colorlist = self.normal_colors
|
191 |
|
192 |
if foreground:
|
193 |
foreground = colorlist[foreground+10]
|
194 |
else:
|
195 |
foreground = self.default_foreground
|
196 |
|
197 |
if background:
|
198 |
background = colorlist[background] |
199 |
else:
|
200 |
|
201 |
if brightness == BRIGHT:
|
202 |
background = wx.BLACK |
203 |
elif brightness == UNDERLINE:
|
204 |
background = wx.YELLOW |
205 |
else:
|
206 |
background = self.default_background
|
207 |
|
208 |
return (foreground, background)
|
209 |
|
210 |
# We must iterate over the entire message string
|
211 |
while char_index < len(message): |
212 |
|
213 |
# First stage: look for a 0x1B character and switch to second stage
|
214 |
if ansi_stage == 1: |
215 |
if message[char_index] == "\x1B": |
216 |
ansi_stage = 2
|
217 |
else:
|
218 |
clean_buffer += message[char_index] |
219 |
|
220 |
# Second stage: look for a validation character (an '[')
|
221 |
elif ansi_stage == 2: |
222 |
if message[char_index] == "[": |
223 |
ansi_stage = 3
|
224 |
ansi_buffer = ""
|
225 |
else:
|
226 |
ansi_stage = 1
|
227 |
|
228 |
# Just add the two last characters
|
229 |
clean_buffer += message[char_index-1:char_index]
|
230 |
|
231 |
# Last stage: process ANSI command with arguments
|
232 |
elif ansi_stage == 3: |
233 |
if message[char_index] in string.digits+";": |
234 |
ansi_buffer += message[char_index] |
235 |
|
236 |
# Now we have all arguments and the command (too generic detection for now...)
|
237 |
else:
|
238 |
ansi_stage = 1
|
239 |
self.modifiers.append((point+len(clean_buffer), select_colors(ansi_buffer))) |
240 |
|
241 |
ansi_buffer = ""
|
242 |
|
243 |
char_index += 1
|
244 |
|
245 |
|
246 |
return clean_buffer
|
247 |
|
248 |
def PostMessage(self, message): |
249 |
"""Applies ANSI style to text"""
|
250 |
|
251 |
start = None
|
252 |
last_mark = None
|
253 |
|
254 |
for point, style in self.modifiers: |
255 |
if not last_mark: |
256 |
last_mark = style |
257 |
start = point |
258 |
continue
|
259 |
|
260 |
# Unpack foreground and background from style tuple
|
261 |
foreground, background = style |
262 |
|
263 |
self.panel.output.SetStyle(start, point, wx.TextAttr(
|
264 |
foreground, background)) |
265 |
|
266 |
start = point |
267 |
last_mark = style |
268 |
|
269 |
self.modifiers = []
|