@pi-unipi/footer 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -0
- package/index.ts +6 -0
- package/package.json +52 -0
- package/src/commands.ts +204 -0
- package/src/config.ts +177 -0
- package/src/events.ts +256 -0
- package/src/index.ts +208 -0
- package/src/presets.ts +131 -0
- package/src/registry/index.ts +162 -0
- package/src/rendering/icons.ts +318 -0
- package/src/rendering/renderer.ts +310 -0
- package/src/rendering/separators.ts +112 -0
- package/src/rendering/theme.ts +98 -0
- package/src/segments/compactor.ts +135 -0
- package/src/segments/core.ts +283 -0
- package/src/segments/kanboard.ts +75 -0
- package/src/segments/mcp.ts +100 -0
- package/src/segments/memory.ts +140 -0
- package/src/segments/notify.ts +50 -0
- package/src/segments/ralph.ts +109 -0
- package/src/segments/status-ext.ts +119 -0
- package/src/segments/workflow.ts +100 -0
- package/src/tui/settings-tui.ts +252 -0
- package/src/types.ts +183 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — FooterRegistry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for segment groups with event subscription,
|
|
5
|
+
* data caching, and reactive updates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FooterGroup } from "../types.js";
|
|
9
|
+
|
|
10
|
+
/** Type for the reactive update callback */
|
|
11
|
+
type UpdateCallback = () => void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* FooterRegistry manages segment groups and their cached data.
|
|
15
|
+
* It subscribes to UNIPI_EVENTS, caches data per group, and
|
|
16
|
+
* notifies subscribers when data changes.
|
|
17
|
+
*/
|
|
18
|
+
export class FooterRegistry {
|
|
19
|
+
/** Registered segment groups */
|
|
20
|
+
private groups = new Map<string, FooterGroup>();
|
|
21
|
+
|
|
22
|
+
/** Cached event data per group */
|
|
23
|
+
private dataCache = new Map<string, unknown>();
|
|
24
|
+
|
|
25
|
+
/** Reactive update subscribers */
|
|
26
|
+
private subscribers = new Set<UpdateCallback>();
|
|
27
|
+
|
|
28
|
+
/** Whether to log debug info */
|
|
29
|
+
private debug: boolean;
|
|
30
|
+
|
|
31
|
+
constructor(options?: { debug?: boolean }) {
|
|
32
|
+
this.debug = options?.debug ?? false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Group Management ─────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Register a segment group.
|
|
39
|
+
*/
|
|
40
|
+
registerGroup(group: FooterGroup): void {
|
|
41
|
+
this.groups.set(group.id, group);
|
|
42
|
+
this.log("registerGroup", group.id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a registered group by ID.
|
|
47
|
+
*/
|
|
48
|
+
getGroup(groupId: string): FooterGroup | undefined {
|
|
49
|
+
return this.groups.get(groupId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all registered groups.
|
|
54
|
+
*/
|
|
55
|
+
getAllGroups(): FooterGroup[] {
|
|
56
|
+
return Array.from(this.groups.values());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Data Cache ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update cached data for a group and notify subscribers.
|
|
63
|
+
*/
|
|
64
|
+
updateData(groupId: string, data: unknown): void {
|
|
65
|
+
const previous = this.dataCache.get(groupId);
|
|
66
|
+
// Only notify if data actually changed (shallow compare)
|
|
67
|
+
if (previous === data) return;
|
|
68
|
+
|
|
69
|
+
this.dataCache.set(groupId, data);
|
|
70
|
+
this.log("updateData", groupId, data);
|
|
71
|
+
this.notifySubscribers();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get cached data for a group.
|
|
76
|
+
*/
|
|
77
|
+
getGroupData(groupId: string): unknown {
|
|
78
|
+
return this.dataCache.get(groupId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear all cached data.
|
|
83
|
+
*/
|
|
84
|
+
invalidateAll(): void {
|
|
85
|
+
this.dataCache.clear();
|
|
86
|
+
this.log("invalidateAll");
|
|
87
|
+
this.notifySubscribers();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Clear cached data for a specific group.
|
|
92
|
+
*/
|
|
93
|
+
invalidateGroup(groupId: string): void {
|
|
94
|
+
this.dataCache.delete(groupId);
|
|
95
|
+
this.log("invalidateGroup", groupId);
|
|
96
|
+
this.notifySubscribers();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Reactive Subscriptions ───────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Subscribe to data updates. Returns an unsubscribe function.
|
|
103
|
+
*/
|
|
104
|
+
subscribe(callback: UpdateCallback): () => void {
|
|
105
|
+
this.subscribers.add(callback);
|
|
106
|
+
return () => {
|
|
107
|
+
this.subscribers.delete(callback);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Notify all subscribers of a data change.
|
|
113
|
+
*/
|
|
114
|
+
private notifySubscribers(): void {
|
|
115
|
+
for (const callback of this.subscribers) {
|
|
116
|
+
try {
|
|
117
|
+
callback();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("[footer] Subscriber error:", err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Debug ────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
private log(event: string, ...args: unknown[]): void {
|
|
127
|
+
if (!this.debug) return;
|
|
128
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
129
|
+
const details = args.length > 0 ? " " + JSON.stringify(args) : "";
|
|
130
|
+
console.error(`[footer-registry:${ts}] ${event}${details}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Singleton ──────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/** Global registry instance */
|
|
137
|
+
let registryInstance: FooterRegistry | null = null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the global FooterRegistry instance.
|
|
141
|
+
* Creates one on first call.
|
|
142
|
+
*/
|
|
143
|
+
export function getFooterRegistry(): FooterRegistry {
|
|
144
|
+
if (!registryInstance) {
|
|
145
|
+
registryInstance = new FooterRegistry();
|
|
146
|
+
}
|
|
147
|
+
return registryInstance;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Reset the global registry (for testing).
|
|
152
|
+
*/
|
|
153
|
+
export function resetFooterRegistry(): void {
|
|
154
|
+
registryInstance = null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Global reference ───────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
// Expose on globalThis for cross-package access
|
|
160
|
+
if (typeof globalThis !== "undefined") {
|
|
161
|
+
(globalThis as Record<string, unknown>).__unipi_footer_registry = getFooterRegistry();
|
|
162
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/footer — Icon system with 3 styles: Nerd Font, Emoji, Text
|
|
3
|
+
*
|
|
4
|
+
* Each icon set maps segment IDs to glyph strings.
|
|
5
|
+
* The active set is determined by the `iconStyle` setting:
|
|
6
|
+
* - "nerd" → Nerd Font glyphs (requires Nerd Font installed)
|
|
7
|
+
* - "emoji" → Unicode emoji / symbols (works on most terminals)
|
|
8
|
+
* - "text" → Plain text labels (works everywhere, most compact)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { detectNerdFontSupport } from "./separators.js";
|
|
12
|
+
|
|
13
|
+
// ─── Icon definitions ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Icon set mapping segment IDs to glyph strings */
|
|
16
|
+
export interface IconSet {
|
|
17
|
+
// Core segments
|
|
18
|
+
model: string;
|
|
19
|
+
thinking: string;
|
|
20
|
+
path: string;
|
|
21
|
+
git: string;
|
|
22
|
+
context: string;
|
|
23
|
+
cost: string;
|
|
24
|
+
tokens: string;
|
|
25
|
+
tokensIn: string;
|
|
26
|
+
tokensOut: string;
|
|
27
|
+
session: string;
|
|
28
|
+
hostname: string;
|
|
29
|
+
time: string;
|
|
30
|
+
|
|
31
|
+
// Compactor segments
|
|
32
|
+
sessionEvents: string;
|
|
33
|
+
compactions: string;
|
|
34
|
+
tokensSaved: string;
|
|
35
|
+
compressionRatio: string;
|
|
36
|
+
indexedDocs: string;
|
|
37
|
+
sandboxRuns: string;
|
|
38
|
+
searchQueries: string;
|
|
39
|
+
|
|
40
|
+
// Memory segments
|
|
41
|
+
projectCount: string;
|
|
42
|
+
totalCount: string;
|
|
43
|
+
consolidations: string;
|
|
44
|
+
|
|
45
|
+
// MCP segments
|
|
46
|
+
serversTotal: string;
|
|
47
|
+
serversActive: string;
|
|
48
|
+
toolsTotal: string;
|
|
49
|
+
serversFailed: string;
|
|
50
|
+
|
|
51
|
+
// Ralph segments
|
|
52
|
+
activeLoops: string;
|
|
53
|
+
totalIterations: string;
|
|
54
|
+
loopStatus: string;
|
|
55
|
+
|
|
56
|
+
// Workflow segments
|
|
57
|
+
currentCommand: string;
|
|
58
|
+
sandboxLevel: string;
|
|
59
|
+
commandDuration: string;
|
|
60
|
+
|
|
61
|
+
// Kanboard segments
|
|
62
|
+
docsCount: string;
|
|
63
|
+
tasksDone: string;
|
|
64
|
+
tasksTotal: string;
|
|
65
|
+
taskPct: string;
|
|
66
|
+
|
|
67
|
+
// Notify segments
|
|
68
|
+
platformsEnabled: string;
|
|
69
|
+
lastSent: string;
|
|
70
|
+
|
|
71
|
+
// Status extension
|
|
72
|
+
extensionStatuses: string;
|
|
73
|
+
|
|
74
|
+
// Separator/group markers
|
|
75
|
+
separator: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Nerd Font icons ────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Nerd Font glyphs — requires a Nerd Font installed in the terminal */
|
|
81
|
+
export const NERD_ICONS: IconSet = {
|
|
82
|
+
// Core
|
|
83
|
+
model: "\uEC19", // nf-md-chip
|
|
84
|
+
thinking: "\uE22C", // nf-oct-pi
|
|
85
|
+
path: "\uF115", // nf-fa-folder_open
|
|
86
|
+
git: "\uF126", // nf-fa-code_fork
|
|
87
|
+
context: "\uE70F", // nf-dev-database
|
|
88
|
+
cost: "\uF155", // nf-fa-dollar
|
|
89
|
+
tokens: "\uE26B", // nf-seti-html
|
|
90
|
+
tokensIn: "\uF090", // nf-fa-sign_in
|
|
91
|
+
tokensOut: "\uF08B", // nf-fa-sign_out
|
|
92
|
+
session: "\uF550", // nf-md-identifier
|
|
93
|
+
hostname: "\uF109", // nf-fa-laptop
|
|
94
|
+
time: "\uF017", // nf-fa-clock_o
|
|
95
|
+
|
|
96
|
+
// Compactor
|
|
97
|
+
sessionEvents: "\uF0C0", // nf-fa-users
|
|
98
|
+
compactions: "\uF1C0", // nf-fa-database
|
|
99
|
+
tokensSaved: "\uF155", // nf-fa-dollar
|
|
100
|
+
compressionRatio:"\uE70F", // nf-dev-database
|
|
101
|
+
indexedDocs: "\uF02D", // nf-fa-book
|
|
102
|
+
sandboxRuns: "\uF121", // nf-fa-terminal
|
|
103
|
+
searchQueries: "\uF002", // nf-fa-search
|
|
104
|
+
|
|
105
|
+
// Memory
|
|
106
|
+
projectCount: "\uee9c", // memory icon
|
|
107
|
+
totalCount: "\uee9c", // memory icon
|
|
108
|
+
consolidations: "\uee9c", // memory icon
|
|
109
|
+
|
|
110
|
+
// MCP
|
|
111
|
+
serversTotal: "\uF233", // nf-fa-server
|
|
112
|
+
serversActive: "\uF058", // nf-fa-check_circle
|
|
113
|
+
toolsTotal: "\uF0AD", // nf-fa-wrench
|
|
114
|
+
serversFailed: "\uF071", // nf-fa-warning
|
|
115
|
+
|
|
116
|
+
// Ralph
|
|
117
|
+
activeLoops: "\udb81\udf09", // ralph loop icon
|
|
118
|
+
totalIterations: "\udb81\udf09", // ralph loop icon
|
|
119
|
+
loopStatus: "\udb81\udf09", // ralph loop icon
|
|
120
|
+
|
|
121
|
+
// Workflow
|
|
122
|
+
currentCommand: "\uf52e", // workflow icon
|
|
123
|
+
sandboxLevel: "\uf023", // nf-fa-lock
|
|
124
|
+
commandDuration: "\uf017", // nf-fa-clock_o
|
|
125
|
+
|
|
126
|
+
// Kanboard
|
|
127
|
+
docsCount: "\uF15C", // nf-fa-file_text
|
|
128
|
+
tasksDone: "\uF058", // nf-fa-check_circle
|
|
129
|
+
tasksTotal: "\uF0AE", // nf-fa-tasks
|
|
130
|
+
taskPct: "\uF200", // nf-fa-pie_chart
|
|
131
|
+
|
|
132
|
+
// Notify
|
|
133
|
+
platformsEnabled:"\uF0E0", // nf-fa-envelope
|
|
134
|
+
lastSent: "\uF017", // nf-fa-clock_o
|
|
135
|
+
|
|
136
|
+
// Extension status
|
|
137
|
+
extensionStatuses:"\uF1E6", // nf-fa-plug
|
|
138
|
+
|
|
139
|
+
separator: "\uE0B1", // nf-pl-left_soft_divider
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ─── Emoji icons ─────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/** Unicode emoji / symbol icons — works on most modern terminals */
|
|
145
|
+
export const EMOJI_ICONS: IconSet = {
|
|
146
|
+
// Core
|
|
147
|
+
model: "",
|
|
148
|
+
thinking: "π",
|
|
149
|
+
path: "",
|
|
150
|
+
git: "⎇",
|
|
151
|
+
context: "",
|
|
152
|
+
cost: "$",
|
|
153
|
+
tokens: "⊛",
|
|
154
|
+
tokensIn: "→",
|
|
155
|
+
tokensOut: "←",
|
|
156
|
+
session: "#",
|
|
157
|
+
hostname: "⌂",
|
|
158
|
+
time: "⏱",
|
|
159
|
+
|
|
160
|
+
// Compactor
|
|
161
|
+
sessionEvents: "⚡",
|
|
162
|
+
compactions: "◧",
|
|
163
|
+
tokensSaved: "$",
|
|
164
|
+
compressionRatio:"⇄",
|
|
165
|
+
indexedDocs: "☰",
|
|
166
|
+
sandboxRuns: "▶",
|
|
167
|
+
searchQueries: "⊗",
|
|
168
|
+
|
|
169
|
+
// Memory
|
|
170
|
+
projectCount: "\uee9c",
|
|
171
|
+
totalCount: "\uee9c",
|
|
172
|
+
consolidations: "\uee9c",
|
|
173
|
+
|
|
174
|
+
// MCP
|
|
175
|
+
serversTotal: "srv",
|
|
176
|
+
serversActive: "●",
|
|
177
|
+
toolsTotal: "🔧",
|
|
178
|
+
serversFailed: "⚠",
|
|
179
|
+
|
|
180
|
+
// Ralph
|
|
181
|
+
activeLoops: "",
|
|
182
|
+
totalIterations: "",
|
|
183
|
+
loopStatus: "",
|
|
184
|
+
|
|
185
|
+
// Workflow
|
|
186
|
+
currentCommand: "",
|
|
187
|
+
sandboxLevel: "◧",
|
|
188
|
+
commandDuration: "⏱",
|
|
189
|
+
|
|
190
|
+
// Kanboard
|
|
191
|
+
docsCount: "☰",
|
|
192
|
+
tasksDone: "✓",
|
|
193
|
+
tasksTotal: "☐",
|
|
194
|
+
taskPct: "%",
|
|
195
|
+
|
|
196
|
+
// Notify
|
|
197
|
+
platformsEnabled:"♮",
|
|
198
|
+
lastSent: "⏱",
|
|
199
|
+
|
|
200
|
+
// Extension status
|
|
201
|
+
extensionStatuses:"▦",
|
|
202
|
+
|
|
203
|
+
separator: "|",
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ─── Text icons ──────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/** Plain text labels — works everywhere, most compact */
|
|
209
|
+
export const TEXT_ICONS: IconSet = {
|
|
210
|
+
// Core
|
|
211
|
+
model: "",
|
|
212
|
+
thinking: "",
|
|
213
|
+
path: "",
|
|
214
|
+
git: "",
|
|
215
|
+
context: "",
|
|
216
|
+
cost: "",
|
|
217
|
+
tokens: "",
|
|
218
|
+
tokensIn: "",
|
|
219
|
+
tokensOut: "",
|
|
220
|
+
session: "",
|
|
221
|
+
hostname: "",
|
|
222
|
+
time: "",
|
|
223
|
+
|
|
224
|
+
// Compactor
|
|
225
|
+
sessionEvents: "evt",
|
|
226
|
+
compactions: "cmp",
|
|
227
|
+
tokensSaved: "svd",
|
|
228
|
+
compressionRatio:"rat",
|
|
229
|
+
indexedDocs: "idx",
|
|
230
|
+
sandboxRuns: "sbx",
|
|
231
|
+
searchQueries: "qry",
|
|
232
|
+
|
|
233
|
+
// Memory
|
|
234
|
+
projectCount: "mem",
|
|
235
|
+
totalCount: "mem",
|
|
236
|
+
consolidations: "cns",
|
|
237
|
+
|
|
238
|
+
// MCP
|
|
239
|
+
serversTotal: "srv",
|
|
240
|
+
serversActive: "act",
|
|
241
|
+
toolsTotal: "tls",
|
|
242
|
+
serversFailed: "err",
|
|
243
|
+
|
|
244
|
+
// Ralph
|
|
245
|
+
activeLoops: "",
|
|
246
|
+
totalIterations: "",
|
|
247
|
+
loopStatus: "",
|
|
248
|
+
|
|
249
|
+
// Workflow
|
|
250
|
+
currentCommand: "",
|
|
251
|
+
sandboxLevel: "sbx",
|
|
252
|
+
commandDuration: "dur",
|
|
253
|
+
|
|
254
|
+
// Kanboard
|
|
255
|
+
docsCount: "doc",
|
|
256
|
+
tasksDone: "✓",
|
|
257
|
+
tasksTotal: "tsk",
|
|
258
|
+
taskPct: "pct",
|
|
259
|
+
|
|
260
|
+
// Notify
|
|
261
|
+
platformsEnabled:"ntf",
|
|
262
|
+
lastSent: "lst",
|
|
263
|
+
|
|
264
|
+
// Extension status
|
|
265
|
+
extensionStatuses:"ext",
|
|
266
|
+
|
|
267
|
+
separator: "|",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// ─── Icon lookup ─────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/** Current icon style — updated by the renderer when settings change */
|
|
273
|
+
let currentIconStyle: "nerd" | "emoji" | "text" | undefined;
|
|
274
|
+
|
|
275
|
+
/** Set the active icon style (called by renderer when settings change) */
|
|
276
|
+
export function setIconStyle(style: "nerd" | "emoji" | "text" | undefined): void {
|
|
277
|
+
currentIconStyle = style;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Resolve the effective icon style from settings + terminal detection */
|
|
281
|
+
export function resolveIconStyle(configured?: string): "nerd" | "emoji" | "text" {
|
|
282
|
+
// Explicit setting wins
|
|
283
|
+
if (configured === "nerd" || configured === "emoji" || configured === "text") {
|
|
284
|
+
return configured;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Auto-detect: use Nerd Font if terminal supports it, emoji otherwise
|
|
288
|
+
return detectNerdFontSupport() ? "nerd" : "emoji";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get the icon for a segment by ID.
|
|
293
|
+
* Uses the configured icon style, falling back to auto-detection.
|
|
294
|
+
*/
|
|
295
|
+
export function getIcon(segmentId: string, overrideStyle?: "nerd" | "emoji" | "text"): string {
|
|
296
|
+
const style = overrideStyle ?? currentIconStyle ?? resolveIconStyle();
|
|
297
|
+
const sets: Record<string, IconSet> = {
|
|
298
|
+
nerd: NERD_ICONS,
|
|
299
|
+
emoji: EMOJI_ICONS,
|
|
300
|
+
text: TEXT_ICONS,
|
|
301
|
+
};
|
|
302
|
+
const icons = sets[style];
|
|
303
|
+
const key = segmentId as keyof IconSet;
|
|
304
|
+
return icons[key] ?? "";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get the full icon set based on the configured style.
|
|
309
|
+
*/
|
|
310
|
+
export function getIcons(iconStyle?: "nerd" | "emoji" | "text"): IconSet {
|
|
311
|
+
const style = iconStyle ?? currentIconStyle ?? resolveIconStyle();
|
|
312
|
+
const sets: Record<string, IconSet> = {
|
|
313
|
+
nerd: NERD_ICONS,
|
|
314
|
+
emoji: EMOJI_ICONS,
|
|
315
|
+
text: TEXT_ICONS,
|
|
316
|
+
};
|
|
317
|
+
return sets[style];
|
|
318
|
+
}
|