-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththeme.py
More file actions
222 lines (197 loc) · 9.66 KB
/
Copy paththeme.py
File metadata and controls
222 lines (197 loc) · 9.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# theme.py - one palette for everything DCCore says in a channel or a notice.
#
# DCCore already had a theme. It was just written eight times.
#
# The same five constants - a red border block, a cyan separator block, a white
# text plate, bold and reset - were redefined as literals INSIDE eight separate
# functions across announce.py and list.py, one per outbound message path:
#
# send_transfer_complete the channel "Sent:" notice
# send_dcc_sending_notice the private "sending you X"
# announce_worker the five-minute channel advertisement
# send_search_result_header private search results
# send_dcc_queue_notice the private queue position
# send_debug the debug channel
# send_pack_error_notice the private error notice
# list.execute_search the search summary
#
# That list IS the whole outbound surface, which is right: one look everywhere
# is exactly what a file server wants. In a busy channel a dozen bots advertise
# at once and the palette is how a person tells them apart at a glance.
#
# The values had not diverged. The SET had: three of the eight defined only
# four of the five names. None of the three read the name it was missing, so
# nothing was broken - but changing DCCore's look meant editing eight places
# and hoping you had found them all, and the drift had already started.
#
# Roles, not colours
# ------------------
# A theme is chosen by name and read as roles, so a preset says what each part
# of a line is FOR rather than which of the sixteen mIRC colours it happens to
# be:
#
# border the outer block that frames a section
# separator the block between fields
# textbox the plate the text sits on
# value a live figure - a count, a speed, a nickname
# alert a figure that is meant to catch the eye
# accent timestamps and secondary text
#
# bold and reset are not roles. They are IRC control characters with fixed
# meanings, and a theme that redefined them would be redefining the protocol.
#
# What a theme does NOT cover
# --------------------------
# send_debug() colours its category tag by MEANING - purple for QUIT, cyan for
# JOIN, grey for INFO, red for SECURITY. Those are semantic, not decorative: an
# operator scanning the debug channel reads the colour before the word, and a
# theme that mapped them all onto one role would erase the distinction they
# exist for. They stay out of the palette, including under "plain" - the debug
# channel is the operator's own, not one where formatting is banned.
#
# config.C_BOLD and config.C_RESET are likewise untouched wherever they appear.
#
# On the line budget
# ------------------
# Colour codes cost bytes against IRC's 512, and announce.fit_irc_line() keeps
# a 420-byte budget for the pessimistic hostmask. A preset must therefore keep
# the SEGMENT COUNT constant - a decorative extra block would eat into the
# filename budget on every line that carries a filename. Presets here change
# which colours the segments are, never how many there are.
import defaults as config
# The mIRC colour pair for a solid block is "fg,bg" with fg == bg: the
# character cell is filled with the background and the (invisible) foreground
# never shows. That is why the classic separator is "10,10" and not "10".
CLASSIC = {
"border": "\x0304,05", # dark red on maroon
"separator": "\x0310,10", # solid cyan
"textbox": "\x0301,00", # black on white
"value": "\x0303", # green
"alert": "\x0304", # red
"accent": "\x0312", # royal blue
}
# Presets are chosen to be distinguishable FROM EACH OTHER AND FROM THE COMMON
# OmenServe and iroffer looks. Two DCCore operators in the same channel who
# both took the default are already indistinguishable, which is the situation
# a theme exists to fix; shipping four presets that all look alike would just
# move the problem.
THEMES = {
"classic": CLASSIC,
# Deep blue plates, amber figures. The furthest from classic's red/cyan
# while staying inside the sixteen colours every client agrees on.
"midnight": {
"border": "\x0302,02", # solid blue
"separator": "\x0312,12", # solid royal blue
"textbox": "\x0300,01", # white on black
"value": "\x0311", # light cyan
"alert": "\x0308", # yellow
"accent": "\x0315", # light grey
},
# Green on black, the terminal look. Reads as deliberate rather than as a
# bot that never got configured.
"forest": {
"border": "\x0303,03", # solid green
"separator": "\x0309,09", # solid light green
"textbox": "\x0301,00", # black on white
"value": "\x0303", # green
"alert": "\x0307", # orange
"accent": "\x0314", # grey
},
# Purple and pink. Loud on purpose - the point of a theme is being picked
# out of a dozen adverts, and an operator who wants that should be able to
# have it without hand-editing codes.
"orchid": {
"border": "\x0306,06", # solid purple
"separator": "\x0313,13", # solid pink
"textbox": "\x0301,00", # black on white
"value": "\x0313", # pink
"alert": "\x0304", # red
"accent": "\x0306", # purple
},
# No codes at all. Deliberately NOT the default: colour is the norm in
# these channels and it is the identity mechanism, so a plain bot is a bot
# nobody picks out. It earns its place for a network or a channel that
# strips or bans formatting.
"plain": {
"border": "",
"separator": "",
"textbox": "",
"value": "",
"alert": "",
"accent": "",
},
}
DEFAULT_THEME = "classic"
# Fixed IRC control characters, not roles. See the note above.
BOLD = "\x02"
RESET = "\x0f"
def theme_name():
"""The configured theme, normalised, falling back to the default.
Loud rather than silent, and the same posture list.list_format() takes: an
unrecognised name is a typo an operator wants to hear about, and the
alternative to falling back is a bot whose every message carries the empty
string where a colour should be.
"""
raw = getattr(config, "THEME", DEFAULT_THEME)
chosen = str(raw or "").strip().lower()
if chosen in THEMES:
return chosen
print(f"[THEME] THEME={raw!r} is not one of {sorted(THEMES)} "
f"- using {DEFAULT_THEME!r}.")
return DEFAULT_THEME
ROLES = ("border", "separator", "textbox", "value", "alert", "accent")
def palette(settings=None):
"""The six roles for the configured theme, as a dict.
config.CUSTOM_THEME_<ROLE> overrides one role on top of the chosen
preset, so somebody who wants one colour changed does not have to restate
the other five. #170's RFC flattened this from a single CUSTOM_THEME dict
into six plain strings (see config.py's own comment on that change) - a
role left at its default of None keeps the preset's own value, exactly
as an absent key in the old dict did.
`settings` answers "what WOULD this look like" without changing anything.
It is a plain {"THEME": ..., "CUSTOM_THEME_ACCENT": ...} mapping, read in
place of config for exactly the keys it holds, so the dashboard can render
a preview of values the operator has typed and not yet saved. A live
daemon is serving a channel while they are choosing colours; the preview
must not reach the config every other thread is reading.
"""
if settings is None:
settings = {}
def setting(name, fallback=None):
if name in settings:
return settings[name]
return getattr(config, name, fallback)
wanted = str(setting("THEME", "") or "").strip().lower()
roles = dict(THEMES[wanted if wanted in THEMES else theme_name()])
for role in roles:
override = setting(f"CUSTOM_THEME_{role.upper()}")
if isinstance(override, str) and override:
# #436: settings_file.py's own writer refuses a newline or a
# null byte in a CUSTOM_THEME_* value now, but this reads
# whatever config actually holds - a hand-edited settings.conf
# or admin_config.py answers to neither that check nor to
# coerce(), and this function is the single place every one of
# the eight outbound templates gets its colours from. Stripped,
# not refused: a preview has nobody to report an error to, and
# the raw preset underneath is always a safe fallback.
override = override.replace("\r", "").replace("\n", "").replace("\x00", "")
if override:
roles[role] = override
return roles
def blocks(settings=None):
"""The eight message paths' palette, in the order they bind it.
Returns (border, separator, textbox, reset, bold, value, alert, accent).
`bold` is still returned and is no longer used by any template. Asked for
directly: "theme shouldn't have bold in any location of the message ...
That's for everywhere bot advertisement answers to find requests etc. No
bolds." It stays in the tuple because it is a protocol constant that
remains true, and because removing it would renumber an unpacking that
eight call sites share for the sake of a name nobody reads.
A tuple rather than the dict because every call site unpacks it into the
local names its templates already use - which is what makes this a change
of where the values come from and not a change to a single line of
outbound text.
"""
roles = palette(settings)
return (roles["border"], roles["separator"], roles["textbox"],
RESET, BOLD, roles["value"], roles["alert"], roles["accent"])