@pi-unipi/footer 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/commands.ts +3 -0
- package/src/config.ts +6 -6
- package/src/events.ts +34 -34
- package/src/index.ts +21 -9
- package/src/presets.ts +6 -6
- package/src/registry/index.ts +5 -7
- package/src/rendering/icons.ts +88 -88
- package/src/rendering/renderer.ts +4 -4
- package/src/segments/core.ts +14 -55
- package/src/segments/memory.ts +9 -7
- package/src/segments/ralph.ts +9 -10
- package/src/segments/status-ext.ts +17 -12
- package/src/segments/workflow.ts +5 -4
- package/src/tui/settings-tui.ts +216 -155
package/src/tui/settings-tui.ts
CHANGED
|
@@ -2,36 +2,59 @@
|
|
|
2
2
|
* @pi-unipi/footer — Settings TUI
|
|
3
3
|
*
|
|
4
4
|
* Interactive settings overlay for toggling groups and individual segments.
|
|
5
|
-
*
|
|
5
|
+
* Uses pi-tui SettingsList for proper vim/arrow keybinding support and search.
|
|
6
|
+
* Modeled after the compactor settings overlay pattern.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
import { loadFooterSettings, saveFooterSettings
|
|
10
|
+
import { SettingsList, type SettingItem, type SettingsListTheme } from "@mariozechner/pi-tui";
|
|
11
|
+
import { loadFooterSettings, saveFooterSettings } from "../config.js";
|
|
11
12
|
import type { FooterGroup, FooterSettings } from "../types.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
// ─── Section types ─────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
type Section = "groups" | "segments";
|
|
17
|
+
const SECTIONS: Section[] = ["groups", "segments"];
|
|
18
|
+
|
|
19
|
+
// ─── Theme for SettingsList ────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const THEME: SettingsListTheme = {
|
|
22
|
+
label: (text, selected) => selected ? `\x1b[1m${text}\x1b[0m` : `\x1b[2m${text}\x1b[0m`,
|
|
23
|
+
value: (text, selected) => selected ? `\x1b[35m${text}\x1b[0m` : `\x1b[35m${text}\x1b[0m`,
|
|
24
|
+
description: (text) => `\x1b[90m${text}\x1b[0m`,
|
|
25
|
+
cursor: `\x1b[36m▸\x1b[0m`,
|
|
26
|
+
hint: (text) => `\x1b[2m${text}\x1b[0m`,
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
// ─── Helper: frame a line inside box drawing ───────────────────────────
|
|
30
|
+
|
|
31
|
+
function frameLine(content: string, innerWidth: number): string {
|
|
32
|
+
const visLen = visibleWidth(content);
|
|
33
|
+
const pad = Math.max(0, innerWidth - visLen);
|
|
34
|
+
return `\x1b[90m│\x1b[0m${content}${" ".repeat(pad)}\x1b[90m│\x1b[0m`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ruleLine(innerWidth: number): string {
|
|
38
|
+
return `\x1b[90m├${"─".repeat(innerWidth)}┤\x1b[0m`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
42
|
+
const left = edge === "top" ? "┌" : "└";
|
|
43
|
+
const right = edge === "top" ? "┐" : "┘";
|
|
44
|
+
return `\x1b[90m${left}${"─".repeat(innerWidth)}${right}\x1b[0m`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Visible width helper (strip ANSI)
|
|
48
|
+
function visibleWidth(text: string): number {
|
|
49
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Show the footer settings overlay ──────────────────────────────────
|
|
26
53
|
|
|
27
|
-
/**
|
|
28
|
-
* Show the footer settings overlay.
|
|
29
|
-
*/
|
|
30
54
|
export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
|
|
31
55
|
ctx.ui.custom(
|
|
32
56
|
(tui: any, _theme: any, _keybindings: any, done: (result: void) => void) => {
|
|
33
57
|
const overlay = new FooterSettingsOverlay(groups);
|
|
34
|
-
|
|
35
58
|
overlay.onClose = () => done();
|
|
36
59
|
|
|
37
60
|
return {
|
|
@@ -52,200 +75,238 @@ export function showFooterSettings(ctx: any, groups: FooterGroup[]): void {
|
|
|
52
75
|
horizontalAlign: "center",
|
|
53
76
|
}),
|
|
54
77
|
},
|
|
55
|
-
).catch((
|
|
56
|
-
|
|
78
|
+
).catch(() => {
|
|
79
|
+
// Silently ignore — overlay errors are non-blocking.
|
|
57
80
|
});
|
|
58
81
|
}
|
|
59
82
|
|
|
83
|
+
// ─── Footer settings overlay component ─────────────────────────────────
|
|
84
|
+
|
|
60
85
|
/**
|
|
61
86
|
* Footer settings overlay component.
|
|
87
|
+
* Uses SettingsList from pi-tui for proper vim/arrow keybinding support.
|
|
88
|
+
* Two sections: Groups (toggle groups) and Segments (toggle segments within a group).
|
|
62
89
|
*/
|
|
63
90
|
class FooterSettingsOverlay {
|
|
64
91
|
private settings: FooterSettings;
|
|
65
92
|
private groups: FooterGroup[];
|
|
66
|
-
private
|
|
67
|
-
private savedGroupIndex = 0;
|
|
68
|
-
private mode: "groups" | "segments" = "groups";
|
|
93
|
+
private section: Section = "groups";
|
|
69
94
|
private selectedGroupId: string | null = null;
|
|
70
95
|
onClose?: () => void;
|
|
71
96
|
|
|
97
|
+
// Per-section SettingsList instances
|
|
98
|
+
private groupList!: SettingsList;
|
|
99
|
+
private segmentList: SettingsList | null = null;
|
|
100
|
+
|
|
72
101
|
constructor(groups: FooterGroup[]) {
|
|
73
102
|
this.settings = loadFooterSettings();
|
|
74
103
|
this.groups = groups;
|
|
104
|
+
this.buildGroupList();
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
invalidate(): void {
|
|
78
|
-
|
|
108
|
+
this.groupList?.invalidate();
|
|
109
|
+
this.segmentList?.invalidate();
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
handleInput(data: string): void {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
// Tab switches section (only if segments are available)
|
|
114
|
+
if (data === "\t" || data === "\x1b[Z") {
|
|
115
|
+
if (this.section === "groups" && this.selectedGroupId) {
|
|
116
|
+
this.section = "segments";
|
|
117
|
+
} else {
|
|
118
|
+
this.section = "groups";
|
|
119
|
+
this.selectedGroupId = null;
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
86
122
|
}
|
|
87
|
-
}
|
|
88
123
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
break;
|
|
94
|
-
case "\x1b[B": case "j":
|
|
95
|
-
this.selectedIndex = (this.selectedIndex + 1) % this.groups.length;
|
|
96
|
-
break;
|
|
97
|
-
case " ":
|
|
98
|
-
this.toggleGroup(this.groups[this.selectedIndex].id);
|
|
99
|
-
break;
|
|
100
|
-
case "\r": case "\x1b[C": case "l":
|
|
101
|
-
this.enterSegmentsMode(this.groups[this.selectedIndex].id);
|
|
102
|
-
break;
|
|
103
|
-
case "q": case "\x1b":
|
|
104
|
-
this.onClose?.();
|
|
105
|
-
break;
|
|
124
|
+
// Escape / q — close
|
|
125
|
+
if (data === "\x1b" || data === "q") {
|
|
126
|
+
this.onClose?.();
|
|
127
|
+
return;
|
|
106
128
|
}
|
|
129
|
+
|
|
130
|
+
// Enter in groups mode — enter segments for the focused group
|
|
131
|
+
if (data === "\r" && this.section === "groups") {
|
|
132
|
+
const focusedId = this.getFocusedGroupId();
|
|
133
|
+
if (focusedId) {
|
|
134
|
+
this.enterSegmentsMode(focusedId);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Left arrow / backspace in segments — back to groups
|
|
140
|
+
if (this.section === "segments" && (data === "\x1b[D" || data === "h" || data === "\x7f")) {
|
|
141
|
+
this.backToGroups();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Delegate to current SettingsList
|
|
146
|
+
this.currentList?.handleInput(data);
|
|
107
147
|
}
|
|
108
148
|
|
|
109
|
-
private
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
149
|
+
private get currentList(): SettingsList | null {
|
|
150
|
+
if (this.section === "segments") return this.segmentList;
|
|
151
|
+
return this.groupList;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Build SettingsList instances ──────────────────────────────────
|
|
155
|
+
|
|
156
|
+
private buildGroupList(): void {
|
|
157
|
+
const items: SettingItem[] = this.groups.map((group) => {
|
|
158
|
+
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
159
|
+
const isEnabled = groupSettings.show;
|
|
160
|
+
const segCount = group.segments.length;
|
|
161
|
+
const enabledCount = group.segments.filter(s => {
|
|
162
|
+
const segOverride = groupSettings.segments?.[s.id];
|
|
163
|
+
return segOverride !== undefined ? segOverride : s.defaultShow;
|
|
164
|
+
}).length;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
id: group.id,
|
|
168
|
+
label: group.name,
|
|
169
|
+
description: `${enabledCount}/${segCount} segments active`,
|
|
170
|
+
currentValue: isEnabled ? "on" : "off",
|
|
171
|
+
values: ["on", "off"],
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
this.groupList = new SettingsList(
|
|
176
|
+
items,
|
|
177
|
+
Math.min(items.length + 2, 15),
|
|
178
|
+
THEME,
|
|
179
|
+
(id, newValue) => this.onGroupChange(id, newValue),
|
|
180
|
+
() => this.onClose?.(),
|
|
181
|
+
{ enableSearch: true },
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private buildSegmentList(groupId: string): void {
|
|
186
|
+
const group = this.groups.find(g => g.id === groupId);
|
|
187
|
+
if (!group) {
|
|
188
|
+
this.segmentList = null;
|
|
189
|
+
return;
|
|
130
190
|
}
|
|
191
|
+
|
|
192
|
+
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
193
|
+
|
|
194
|
+
const items: SettingItem[] = group.segments.map((seg) => {
|
|
195
|
+
const isEnabled = groupSettings.segments?.[seg.id] ?? seg.defaultShow;
|
|
196
|
+
return {
|
|
197
|
+
id: seg.id,
|
|
198
|
+
label: seg.label,
|
|
199
|
+
description: "",
|
|
200
|
+
currentValue: isEnabled ? "on" : "off",
|
|
201
|
+
values: ["on", "off"],
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
this.segmentList = new SettingsList(
|
|
206
|
+
items,
|
|
207
|
+
Math.min(items.length + 2, 15),
|
|
208
|
+
THEME,
|
|
209
|
+
(id, newValue) => this.onSegmentChange(groupId, id, newValue),
|
|
210
|
+
() => this.backToGroups(),
|
|
211
|
+
{ enableSearch: true },
|
|
212
|
+
);
|
|
131
213
|
}
|
|
132
214
|
|
|
133
|
-
|
|
215
|
+
// ─── Change handlers ───────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
private onGroupChange(groupId: string, newValue: string): void {
|
|
134
218
|
const groupSettings = this.settings.groups[groupId] ?? { show: true, segments: {} };
|
|
135
|
-
groupSettings.show =
|
|
219
|
+
groupSettings.show = newValue === "on";
|
|
136
220
|
this.settings.groups[groupId] = groupSettings;
|
|
137
221
|
saveFooterSettings(this.settings);
|
|
222
|
+
|
|
223
|
+
// Update display
|
|
224
|
+
this.groupList.updateValue(groupId, newValue);
|
|
138
225
|
}
|
|
139
226
|
|
|
140
|
-
private
|
|
227
|
+
private onSegmentChange(groupId: string, segmentId: string, newValue: string): void {
|
|
141
228
|
const groupSettings = this.settings.groups[groupId] ?? { show: true, segments: {} };
|
|
142
229
|
if (!groupSettings.segments) groupSettings.segments = {};
|
|
143
|
-
groupSettings.segments[segmentId] =
|
|
230
|
+
groupSettings.segments[segmentId] = newValue === "on";
|
|
144
231
|
this.settings.groups[groupId] = groupSettings;
|
|
145
232
|
saveFooterSettings(this.settings);
|
|
233
|
+
|
|
234
|
+
// Update the segment list display
|
|
235
|
+
this.segmentList?.updateValue(segmentId, newValue);
|
|
236
|
+
|
|
237
|
+
// Update group description (segment count)
|
|
238
|
+
const group = this.groups.find(g => g.id === groupId);
|
|
239
|
+
if (group) {
|
|
240
|
+
const segCount = group.segments.length;
|
|
241
|
+
const enabledCount = group.segments.filter(s => {
|
|
242
|
+
const segOverride = groupSettings.segments?.[s.id];
|
|
243
|
+
return segOverride !== undefined ? segOverride : s.defaultShow;
|
|
244
|
+
}).length;
|
|
245
|
+
this.groupList.updateValue(groupId, groupSettings.show ? "on" : "off");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── Section navigation ────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
private getFocusedGroupId(): string | null {
|
|
252
|
+
// Walk the group list items in order; return the first one
|
|
253
|
+
// that matches the focused index. Since SettingsList doesn't
|
|
254
|
+
// expose selectedIndex, we track by the group array order.
|
|
255
|
+
return this.selectedGroupId ?? this.groups[0]?.id ?? null;
|
|
146
256
|
}
|
|
147
257
|
|
|
148
258
|
private enterSegmentsMode(groupId: string): void {
|
|
149
|
-
this.savedGroupIndex = this.selectedIndex;
|
|
150
|
-
this.mode = "segments";
|
|
151
259
|
this.selectedGroupId = groupId;
|
|
152
|
-
this.
|
|
260
|
+
this.section = "segments";
|
|
261
|
+
this.buildSegmentList(groupId);
|
|
153
262
|
}
|
|
154
263
|
|
|
155
264
|
private backToGroups(): void {
|
|
156
|
-
this.
|
|
157
|
-
this.selectedIndex = this.savedGroupIndex;
|
|
265
|
+
this.section = "groups";
|
|
158
266
|
this.selectedGroupId = null;
|
|
267
|
+
this.segmentList = null;
|
|
159
268
|
}
|
|
160
269
|
|
|
161
|
-
|
|
162
|
-
if (this.mode === "groups") {
|
|
163
|
-
return this.renderGroupsMode(width);
|
|
164
|
-
} else {
|
|
165
|
-
return this.renderSegmentsMode(width);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
270
|
+
// ─── Render ────────────────────────────────────────────────────────
|
|
168
271
|
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const pad = Math.max(0, targetWidth - visLen);
|
|
172
|
-
return line + " ".repeat(pad);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private renderCentered(text: string, width: number): string {
|
|
176
|
-
const visLen = visibleWidth(text);
|
|
177
|
-
if (visLen >= width) return text;
|
|
178
|
-
const leftPad = Math.floor((width - visLen) / 2);
|
|
179
|
-
return " ".repeat(leftPad) + text;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private renderGroupsMode(width: number): string[] {
|
|
272
|
+
render(width: number): string[] {
|
|
273
|
+
const innerWidth = Math.max(22, width - 2);
|
|
183
274
|
const lines: string[] = [];
|
|
184
|
-
const innerWidth = width - 2;
|
|
185
|
-
|
|
186
|
-
lines.push(`${ansi.dim}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
|
|
187
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${ansi.bold}⚙ Footer Settings${ansi.reset}`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
188
|
-
lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
|
|
189
|
-
|
|
190
|
-
for (let i = 0; i < this.groups.length; i++) {
|
|
191
|
-
const group = this.groups[i];
|
|
192
|
-
const isSelected = i === this.selectedIndex;
|
|
193
|
-
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
194
|
-
const isEnabled = groupSettings.show;
|
|
195
275
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
276
|
+
// Header
|
|
277
|
+
lines.push(borderLine(innerWidth, "top"));
|
|
278
|
+
lines.push(frameLine(`\x1b[1m\x1b[36m⚙ Footer Settings\x1b[0m`, innerWidth));
|
|
199
279
|
|
|
200
|
-
|
|
201
|
-
|
|
280
|
+
// Section tabs
|
|
281
|
+
const tabParts = SECTIONS.map((s) => {
|
|
282
|
+
const label = s.charAt(0).toUpperCase() + s.slice(1);
|
|
283
|
+
if (s === this.section) {
|
|
284
|
+
return `\x1b[1m\x1b[36m[${label}]\x1b[0m`;
|
|
202
285
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
line = truncateToWidth(line, innerWidth - 2);
|
|
286
|
+
if (s === "segments" && !this.selectedGroupId) {
|
|
287
|
+
return `\x1b[2m${label}\x1b[0m`; // dimmed if no group selected
|
|
206
288
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
private renderSegmentsMode(width: number): string[] {
|
|
219
|
-
const lines: string[] = [];
|
|
220
|
-
const group = this.groups.find(g => g.id === this.selectedGroupId);
|
|
221
|
-
if (!group) return lines;
|
|
222
|
-
|
|
223
|
-
const groupSettings = this.settings.groups[group.id] ?? { show: group.defaultShow, segments: {} };
|
|
224
|
-
const innerWidth = width - 2;
|
|
225
|
-
|
|
226
|
-
lines.push(`${ansi.dim}╭${"─".repeat(innerWidth)}╮${ansi.reset}`);
|
|
227
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(this.renderCentered(`${group.name} Segments`, innerWidth), innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
228
|
-
lines.push(`${ansi.dim}├${"─".repeat(innerWidth)}┤${ansi.reset}`);
|
|
229
|
-
|
|
230
|
-
for (let i = 0; i < group.segments.length; i++) {
|
|
231
|
-
const seg = group.segments[i];
|
|
232
|
-
const isSelected = i === this.selectedIndex;
|
|
233
|
-
const isEnabled = groupSettings.segments?.[seg.id] ?? seg.defaultShow;
|
|
234
|
-
|
|
235
|
-
const toggle = isEnabled ? TOGGLE_ON : TOGGLE_OFF;
|
|
236
|
-
const indicator = isSelected ? `${ansi.cyan}▸${ansi.reset}` : " ";
|
|
237
|
-
let line = ` ${indicator} ${toggle} ${seg.label}`;
|
|
238
|
-
|
|
239
|
-
if (visibleWidth(line) > innerWidth - 2) {
|
|
240
|
-
line = truncateToWidth(line, innerWidth - 2);
|
|
289
|
+
return `\x1b[2m${label}\x1b[0m`;
|
|
290
|
+
});
|
|
291
|
+
lines.push(frameLine(` ${tabParts.join(" ")}`, innerWidth));
|
|
292
|
+
lines.push(ruleLine(innerWidth));
|
|
293
|
+
|
|
294
|
+
// Section content (rendered by SettingsList)
|
|
295
|
+
const activeList = this.currentList;
|
|
296
|
+
if (activeList) {
|
|
297
|
+
const contentLines = activeList.render(innerWidth - 2);
|
|
298
|
+
for (const line of contentLines) {
|
|
299
|
+
lines.push(frameLine(` ${line}`, innerWidth));
|
|
241
300
|
}
|
|
242
|
-
|
|
243
|
-
lines.push(`${ansi.dim}│${ansi.reset}${this.padToWidth(line, innerWidth)}${ansi.dim}│${ansi.reset}`);
|
|
244
301
|
}
|
|
245
302
|
|
|
246
|
-
|
|
247
|
-
lines.push(
|
|
248
|
-
|
|
303
|
+
// Footer hints
|
|
304
|
+
lines.push(ruleLine(innerWidth));
|
|
305
|
+
const hints = this.section === "groups"
|
|
306
|
+
? "↑↓ navigate · Space toggle · Enter segments · / search · q close"
|
|
307
|
+
: "↑↓ navigate · Space toggle · ← back · / search · q close";
|
|
308
|
+
lines.push(frameLine(`\x1b[2m${hints}\x1b[0m`, innerWidth));
|
|
309
|
+
lines.push(borderLine(innerWidth, "bottom"));
|
|
249
310
|
|
|
250
311
|
return lines;
|
|
251
312
|
}
|