@fresh-editor/fresh-editor 0.2.22 → 0.2.23

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/plugins/pkg.ts CHANGED
@@ -1654,6 +1654,10 @@ interface PkgManagerState {
1654
1654
  selectedIndex: number;
1655
1655
  focus: FocusTarget; // What element has Tab focus
1656
1656
  isLoading: boolean;
1657
+ viewportHeight: number;
1658
+ // Buffer group fields
1659
+ groupId: number | null;
1660
+ panelBuffers: Record<string, number>;
1657
1661
  }
1658
1662
 
1659
1663
  const pkgState: PkgManagerState = {
@@ -1667,6 +1671,9 @@ const pkgState: PkgManagerState = {
1667
1671
  selectedIndex: 0,
1668
1672
  focus: { type: "list" },
1669
1673
  isLoading: false,
1674
+ viewportHeight: 24,
1675
+ groupId: null,
1676
+ panelBuffers: {},
1670
1677
  };
1671
1678
 
1672
1679
  // Theme-aware color configuration
@@ -1931,9 +1938,9 @@ function formatNumber(n: number | undefined): string {
1931
1938
  }
1932
1939
 
1933
1940
  // Layout constants
1934
- const LIST_WIDTH = 36; // Width of left panel (package list)
1935
- const TOTAL_WIDTH = 88; // Total width of UI
1936
- const DETAIL_WIDTH = TOTAL_WIDTH - LIST_WIDTH - 3; // Right panel width (minus divider)
1941
+ const TOTAL_WIDTH = 88;
1942
+ const LIST_WIDTH = 36;
1943
+ function DETAIL_WIDTH(): number { return TOTAL_WIDTH - LIST_WIDTH - 3; }
1937
1944
 
1938
1945
  /**
1939
1946
  * Helper to check if a button is focused
@@ -1987,103 +1994,60 @@ function wrapText(text: string, maxWidth: number): string[] {
1987
1994
  /**
1988
1995
  * Build virtual buffer entries for the package manager (split-view layout)
1989
1996
  */
1990
- function buildListViewEntries(): TextPropertyEntry[] {
1997
+ function utf8ByteLength(str: string): number {
1998
+ let bytes = 0;
1999
+ for (let i = 0; i < str.length; i++) {
2000
+ const code = str.charCodeAt(i);
2001
+ if (code < 0x80) {
2002
+ bytes += 1;
2003
+ } else if (code < 0x800) {
2004
+ bytes += 2;
2005
+ } else if (code >= 0xD800 && code <= 0xDBFF) {
2006
+ // Surrogate pair = 4 bytes, skip low surrogate
2007
+ bytes += 4;
2008
+ i++;
2009
+ } else {
2010
+ bytes += 3;
2011
+ }
2012
+ }
2013
+ return bytes;
2014
+ }
2015
+
2016
+ function buildPkgHeaderEntries(): TextPropertyEntry[] {
1991
2017
  const entries: TextPropertyEntry[] = [];
2018
+ entries.push({ text: " Packages\n", properties: { type: "header" } });
2019
+ // Search bar
2020
+ const searchFocused = pkgState.focus.type === "search";
2021
+ const searchLeft = searchFocused ? "[" : " ";
2022
+ const searchRight = searchFocused ? "]" : " ";
2023
+ const searchVal = pkgState.searchQuery || "";
2024
+ entries.push({ text: ` Search: ${searchLeft}${searchVal.padEnd(30)}${searchRight}\n`, properties: { type: "search-input", focused: searchFocused } });
2025
+ // Filter bar
2026
+ const filters = ["All", "Installed", "Plugins", "Themes", "Languages", "Bundles"];
2027
+ let filterLine = " ";
2028
+ for (let i = 0; i < filters.length; i++) {
2029
+ const isActive = pkgState.filter === filters[i].toLowerCase();
2030
+ const isFocused = pkgState.focus.type === "filter" && pkgState.focus.index === i;
2031
+ const lb = isFocused ? "[" : " ";
2032
+ const rb = isFocused ? "]" : " ";
2033
+ filterLine += `${lb} ${filters[i]} ${rb} `;
2034
+ }
2035
+ const syncFocused = pkgState.focus.type === "sync";
2036
+ const sl = syncFocused ? "[" : " ";
2037
+ const sr = syncFocused ? "]" : " ";
2038
+ filterLine += ` ${sl} Sync ${sr}`;
2039
+ entries.push({ text: filterLine + "\n", properties: { type: "filter-bar" } });
2040
+ return entries;
2041
+ }
2042
+
2043
+ function buildPkgListEntries(): TextPropertyEntry[] {
1992
2044
  const items = getFilteredItems();
1993
- const selectedItem = items.length > 0 && pkgState.selectedIndex < items.length
1994
- ? items[pkgState.selectedIndex] : null;
1995
2045
  const installedItems = items.filter(i => i.installed);
1996
2046
  const availableItems = items.filter(i => !i.installed);
2047
+ const entries: TextPropertyEntry[] = [];
1997
2048
 
1998
- // === HEADER ===
1999
- entries.push({
2000
- text: " Packages\n",
2001
- properties: { type: "header" },
2002
- });
2003
-
2004
- // Empty line after header
2005
- entries.push({ text: "\n", properties: { type: "blank" } });
2006
-
2007
- // === SEARCH BAR (input-style) ===
2008
- const searchFocused = isButtonFocused("search");
2009
- const searchInputWidth = 30;
2010
- const searchText = pkgState.searchQuery || "";
2011
- const searchDisplay = searchText.length > searchInputWidth - 1
2012
- ? searchText.slice(0, searchInputWidth - 2) + "…"
2013
- : searchText.padEnd(searchInputWidth);
2014
-
2015
- entries.push({ text: " Search: ", properties: { type: "search-label" } });
2016
- entries.push({
2017
- text: searchFocused ? `[${searchDisplay}]` : ` ${searchDisplay} `,
2018
- properties: { type: "search-input", focused: searchFocused },
2019
- });
2020
- entries.push({ text: "\n", properties: { type: "newline" } });
2021
-
2022
- // === FILTER BAR with focusable buttons ===
2023
- const filters: Array<{ id: string; label: string }> = [
2024
- { id: "all", label: "All" },
2025
- { id: "installed", label: "Installed" },
2026
- { id: "plugins", label: "Plugins" },
2027
- { id: "themes", label: "Themes" },
2028
- { id: "languages", label: "Languages" },
2029
- { id: "bundles", label: "Bundles" },
2030
- ];
2031
-
2032
- // Build filter buttons with position tracking
2033
- let filterBarParts: Array<{ text: string; type: string; focused?: boolean; active?: boolean }> = [];
2034
- filterBarParts.push({ text: " ", type: "spacer" });
2035
-
2036
- for (let i = 0; i < filters.length; i++) {
2037
- const f = filters[i];
2038
- const isActive = pkgState.filter === f.id;
2039
- const isFocused = isButtonFocused("filter", i);
2040
- // Always reserve space for brackets - show [ ] when focused, spaces when not
2041
- const leftBracket = isFocused ? "[" : " ";
2042
- const rightBracket = isFocused ? "]" : " ";
2043
- filterBarParts.push({
2044
- text: `${leftBracket} ${f.label} ${rightBracket}`,
2045
- type: "filter-btn",
2046
- focused: isFocused,
2047
- active: isActive,
2048
- });
2049
- }
2050
-
2051
- filterBarParts.push({ text: " ", type: "spacer" });
2052
-
2053
- // Sync button - always reserve space for brackets
2054
- const syncFocused = isButtonFocused("sync");
2055
- const syncLeft = syncFocused ? "[" : " ";
2056
- const syncRight = syncFocused ? "]" : " ";
2057
- filterBarParts.push({ text: `${syncLeft} Sync ${syncRight}`, type: "sync-btn", focused: syncFocused });
2058
-
2059
- // Emit each filter bar part as separate entry for individual styling
2060
- for (const part of filterBarParts) {
2061
- entries.push({
2062
- text: part.text,
2063
- properties: {
2064
- type: part.type,
2065
- focused: part.focused,
2066
- active: part.active,
2067
- },
2068
- });
2069
- }
2070
- entries.push({ text: "\n", properties: { type: "newline" } });
2071
-
2072
- // === TOP SEPARATOR ===
2073
- entries.push({
2074
- text: " " + "─".repeat(TOTAL_WIDTH - 2) + "\n",
2075
- properties: { type: "separator" },
2076
- });
2077
-
2078
- // === SPLIT VIEW: Package list on left, Details on right ===
2079
-
2080
- // Build left panel lines (package list)
2081
- const leftLines: Array<{ text: string; type: string; selected?: boolean; installed?: boolean }> = [];
2082
-
2083
- // Installed section
2084
2049
  if (installedItems.length > 0) {
2085
- leftLines.push({ text: `INSTALLED (${installedItems.length})`, type: "section-title" });
2086
-
2050
+ entries.push({ text: `INSTALLED (${installedItems.length})\n`, properties: { type: "section-title" } });
2087
2051
  let idx = 0;
2088
2052
  for (const item of installedItems) {
2089
2053
  const isSelected = idx === pkgState.selectedIndex;
@@ -2091,335 +2055,101 @@ function buildListViewEntries(): TextPropertyEntry[] {
2091
2055
  const prefix = isSelected && listFocused ? "▸" : " ";
2092
2056
  const status = item.updateAvailable ? "↑" : "✓";
2093
2057
  const ver = item.version.length > 7 ? item.version.slice(0, 6) + "…" : item.version;
2094
- const name = item.name.length > 18 ? item.name.slice(0, 17) + "…" : item.name;
2095
- const line = `${prefix} ${name.padEnd(18)} ${ver.padEnd(7)} ${status}`;
2096
- leftLines.push({ text: line, type: "package-row", selected: isSelected, installed: true });
2058
+ const nameW = Math.max(8, LIST_WIDTH - 16);
2059
+ const name = item.name.length > nameW ? item.name.slice(0, nameW - 1) + "…" : item.name;
2060
+ entries.push({ text: `${prefix} ${name.padEnd(nameW)} ${ver.padEnd(7)} ${status}\n`, properties: { type: "package-row", selected: isSelected, installed: true } });
2097
2061
  idx++;
2098
2062
  }
2099
2063
  }
2100
2064
 
2101
- // Available section
2102
2065
  if (availableItems.length > 0) {
2103
- if (leftLines.length > 0) leftLines.push({ text: "", type: "blank" });
2104
- leftLines.push({ text: `AVAILABLE (${availableItems.length})`, type: "section-title" });
2105
-
2066
+ if (entries.length > 0) entries.push({ text: "\n", properties: { type: "blank" } });
2067
+ entries.push({ text: `AVAILABLE (${availableItems.length})\n`, properties: { type: "section-title" } });
2106
2068
  let idx = installedItems.length;
2107
2069
  for (const item of availableItems) {
2108
2070
  const isSelected = idx === pkgState.selectedIndex;
2109
2071
  const listFocused = pkgState.focus.type === "list";
2110
2072
  const prefix = isSelected && listFocused ? "▸" : " ";
2111
2073
  const typeTag = item.packageType === "theme" ? "T" : item.packageType === "language" ? "L" : item.packageType === "bundle" ? "B" : "P";
2112
- const name = item.name.length > 22 ? item.name.slice(0, 21) + "…" : item.name;
2113
- const line = `${prefix} ${name.padEnd(22)} [${typeTag}]`;
2114
- leftLines.push({ text: line, type: "package-row", selected: isSelected, installed: false });
2074
+ const availNameW = Math.max(8, LIST_WIDTH - 10);
2075
+ const name = item.name.length > availNameW ? item.name.slice(0, availNameW - 1) + "…" : item.name;
2076
+ entries.push({ text: `${prefix} ${name.padEnd(availNameW)} [${typeTag}]\n`, properties: { type: "package-row", selected: isSelected, installed: false } });
2115
2077
  idx++;
2116
2078
  }
2117
2079
  }
2118
2080
 
2119
- // Empty state for left panel
2120
2081
  if (items.length === 0) {
2121
2082
  if (pkgState.isLoading) {
2122
- leftLines.push({ text: "Loading...", type: "empty-state" });
2123
- } else if (!isRegistrySynced()) {
2124
- leftLines.push({ text: "Registry not synced", type: "empty-state" });
2125
- leftLines.push({ text: "Tab to Sync button", type: "empty-state" });
2083
+ entries.push({ text: "Loading...\n", properties: { type: "empty-state" } });
2126
2084
  } else {
2127
- leftLines.push({ text: "No packages found", type: "empty-state" });
2085
+ entries.push({ text: "No packages found\n", properties: { type: "empty-state" } });
2128
2086
  }
2129
2087
  }
2130
2088
 
2131
- // Build right panel lines (details for selected package)
2132
- const rightLines: Array<{ text: string; type: string; focused?: boolean; btnIndex?: number }> = [];
2089
+ return entries;
2090
+ }
2133
2091
 
2134
- if (selectedItem) {
2135
- // Package name
2136
- rightLines.push({ text: selectedItem.name, type: "detail-title" });
2137
- rightLines.push({ text: "─".repeat(Math.min(selectedItem.name.length + 2, DETAIL_WIDTH - 2)), type: "detail-sep" });
2092
+ function buildPkgDetailEntries(): TextPropertyEntry[] {
2093
+ const items = getFilteredItems();
2094
+ const selectedItem = items.length > 0 && pkgState.selectedIndex < items.length
2095
+ ? items[pkgState.selectedIndex] : null;
2096
+ const entries: TextPropertyEntry[] = [];
2138
2097
 
2139
- // Version / Author / License on one line
2098
+ if (selectedItem) {
2099
+ entries.push({ text: selectedItem.name + "\n", properties: { type: "detail-title" } });
2100
+ entries.push({ text: "─".repeat(Math.min(selectedItem.name.length + 2, 50)) + "\n", properties: { type: "detail-sep" } });
2140
2101
  let metaLine = `v${selectedItem.version}`;
2141
2102
  if (selectedItem.author) metaLine += ` • ${selectedItem.author}`;
2142
2103
  if (selectedItem.license) metaLine += ` • ${selectedItem.license}`;
2143
- if (metaLine.length > DETAIL_WIDTH - 2) metaLine = metaLine.slice(0, DETAIL_WIDTH - 5) + "...";
2144
- rightLines.push({ text: metaLine, type: "detail-meta" });
2145
-
2146
- rightLines.push({ text: "", type: "blank" });
2147
-
2148
- // Description (wrapped)
2104
+ entries.push({ text: metaLine + "\n", properties: { type: "detail-meta" } });
2105
+ entries.push({ text: "\n", properties: { type: "blank" } });
2149
2106
  const descText = selectedItem.description || "No description available";
2150
- const descLines = wrapText(descText, DETAIL_WIDTH - 2);
2107
+ const descLines = wrapText(descText, 50);
2151
2108
  for (const line of descLines) {
2152
- rightLines.push({ text: line, type: "detail-desc" });
2109
+ entries.push({ text: line + "\n", properties: { type: "detail-desc" } });
2153
2110
  }
2154
-
2155
- rightLines.push({ text: "", type: "blank" });
2156
-
2157
- // Keywords
2111
+ entries.push({ text: "\n", properties: { type: "blank" } });
2158
2112
  if (selectedItem.keywords && selectedItem.keywords.length > 0) {
2159
- const kwText = selectedItem.keywords.slice(0, 4).join(", ");
2160
- rightLines.push({ text: `Tags: ${kwText}`, type: "detail-tags" });
2161
- rightLines.push({ text: "", type: "blank" });
2113
+ entries.push({ text: `Tags: ${selectedItem.keywords.slice(0, 4).join(", ")}\n`, properties: { type: "detail-tags" } });
2114
+ entries.push({ text: "\n", properties: { type: "blank" } });
2162
2115
  }
2163
-
2164
- // Repository URL
2165
2116
  if (selectedItem.repository) {
2166
- // Shorten URL for display (remove protocol, truncate if needed)
2167
- let displayUrl = selectedItem.repository
2168
- .replace(/^https?:\/\//, "")
2169
- .replace(/\.git$/, "");
2170
- if (displayUrl.length > DETAIL_WIDTH - 2) {
2171
- displayUrl = displayUrl.slice(0, DETAIL_WIDTH - 5) + "...";
2172
- }
2173
- rightLines.push({ text: displayUrl, type: "detail-url" });
2174
- rightLines.push({ text: "", type: "blank" });
2117
+ let displayUrl = selectedItem.repository.replace(/^https?:\/\//, "").replace(/\.git$/, "");
2118
+ if (displayUrl.length > 50) displayUrl = displayUrl.slice(0, 47) + "...";
2119
+ entries.push({ text: displayUrl + "\n", properties: { type: "detail-url" } });
2120
+ entries.push({ text: "\n", properties: { type: "blank" } });
2175
2121
  }
2176
-
2177
- // Action buttons - always reserve space for brackets
2178
2122
  const actions = getActionButtons();
2179
2123
  for (let i = 0; i < actions.length; i++) {
2180
- const focused = isButtonFocused("action", i);
2181
- const leftBracket = focused ? "[" : " ";
2182
- const rightBracket = focused ? "]" : " ";
2183
- const btnText = `${leftBracket} ${actions[i]} ${rightBracket}`;
2184
- rightLines.push({ text: btnText, type: "action-btn", focused, btnIndex: i });
2124
+ const focused = pkgState.focus.type === "action" && pkgState.focus.index === i;
2125
+ const lb = focused ? "[" : " ";
2126
+ const rb = focused ? "]" : " ";
2127
+ entries.push({ text: `${lb} ${actions[i]} ${rb}\n`, properties: { type: "action-btn", focused, btnIndex: i } });
2185
2128
  }
2186
2129
  } else {
2187
- rightLines.push({ text: "Select a package", type: "empty-state" });
2188
- rightLines.push({ text: "to view details", type: "empty-state" });
2189
- }
2190
-
2191
- // Merge left and right panels into rows
2192
- const maxRows = Math.max(leftLines.length, rightLines.length, 8);
2193
- for (let i = 0; i < maxRows; i++) {
2194
- const leftItem = leftLines[i];
2195
- const rightItem = rightLines[i];
2196
-
2197
- // Left side (padded to fixed width)
2198
- const leftText = leftItem ? (" " + leftItem.text) : "";
2199
- entries.push({
2200
- text: leftText.padEnd(LIST_WIDTH),
2201
- properties: {
2202
- type: leftItem?.type || "blank",
2203
- selected: leftItem?.selected,
2204
- installed: leftItem?.installed,
2205
- },
2206
- });
2207
-
2208
- // Divider
2209
- entries.push({ text: "│", properties: { type: "divider" } });
2210
-
2211
- // Right side
2212
- const rightText = rightItem ? (" " + rightItem.text) : "";
2213
- entries.push({
2214
- text: rightText,
2215
- properties: {
2216
- type: rightItem?.type || "blank",
2217
- focused: rightItem?.focused,
2218
- btnIndex: rightItem?.btnIndex,
2219
- },
2220
- });
2221
-
2222
- entries.push({ text: "\n", properties: { type: "newline" } });
2223
- }
2224
-
2225
- // === BOTTOM SEPARATOR ===
2226
- entries.push({
2227
- text: " " + "─".repeat(TOTAL_WIDTH - 2) + "\n",
2228
- properties: { type: "separator" },
2229
- });
2230
-
2231
- // === HELP LINE ===
2232
- let helpText = " ↑↓ Navigate Tab Next / Search Enter ";
2233
- if (pkgState.focus.type === "action") {
2234
- helpText += "Activate";
2235
- } else if (pkgState.focus.type === "filter") {
2236
- helpText += "Filter";
2237
- } else if (pkgState.focus.type === "sync") {
2238
- helpText += "Sync";
2239
- } else if (pkgState.focus.type === "search") {
2240
- helpText += "Search";
2241
- } else {
2242
- helpText += "Select";
2130
+ entries.push({ text: "Select a package\nto view details\n", properties: { type: "empty-state" } });
2243
2131
  }
2244
- helpText += " Esc Close\n";
2245
-
2246
- entries.push({
2247
- text: helpText,
2248
- properties: { type: "help" },
2249
- });
2250
2132
 
2251
2133
  return entries;
2252
2134
  }
2253
2135
 
2254
- /**
2255
- * Calculate UTF-8 byte length of a string.
2256
- * Needed because string.length returns character count, not byte count.
2257
- * Unicode chars like and ─ are 1 char but 3 bytes in UTF-8.
2258
- */
2259
- function utf8ByteLength(str: string): number {
2260
- let bytes = 0;
2261
- for (let i = 0; i < str.length; i++) {
2262
- const code = str.charCodeAt(i);
2263
- if (code < 0x80) {
2264
- bytes += 1;
2265
- } else if (code < 0x800) {
2266
- bytes += 2;
2267
- } else if (code >= 0xD800 && code <= 0xDBFF) {
2268
- // Surrogate pair = 4 bytes, skip low surrogate
2269
- bytes += 4;
2270
- i++;
2271
- } else {
2272
- bytes += 3;
2273
- }
2274
- }
2275
- return bytes;
2276
- }
2277
-
2278
- /**
2279
- * Apply theme-aware highlighting to the package manager view
2280
- */
2281
- function applyPkgManagerHighlighting(): void {
2282
- if (pkgState.bufferId === null) return;
2283
-
2284
- // Clear existing overlays
2285
- editor.clearNamespace(pkgState.bufferId, "pkg");
2286
-
2287
- const entries = buildListViewEntries();
2288
- let byteOffset = 0;
2289
-
2290
- for (const entry of entries) {
2291
- const props = entry.properties as Record<string, unknown>;
2292
- const len = utf8ByteLength(entry.text);
2293
-
2294
- // Determine theme colors based on entry type
2295
- let themeStyle: ThemeColor | null = null;
2296
-
2297
- switch (props.type) {
2298
- case "header":
2299
- themeStyle = pkgTheme.header;
2300
- break;
2301
-
2302
- case "section-title":
2303
- themeStyle = pkgTheme.sectionTitle;
2304
- break;
2305
-
2306
- case "filter-btn":
2307
- if (props.focused && props.active) {
2308
- // Both focused and active - use focused style
2309
- themeStyle = pkgTheme.buttonFocused;
2310
- } else if (props.focused) {
2311
- // Only focused (not the active filter)
2312
- themeStyle = pkgTheme.filterFocused;
2313
- } else if (props.active) {
2314
- // Active filter but not focused
2315
- themeStyle = pkgTheme.filterActive;
2316
- } else {
2317
- themeStyle = pkgTheme.filterInactive;
2318
- }
2319
- break;
2320
-
2321
- case "sync-btn":
2322
- themeStyle = props.focused ? pkgTheme.buttonFocused : pkgTheme.button;
2323
- break;
2324
-
2325
- case "search-label":
2326
- themeStyle = pkgTheme.infoLabel;
2327
- break;
2328
-
2329
- case "search-input":
2330
- // Search input field styling - distinct background
2331
- themeStyle = props.focused ? pkgTheme.searchBoxFocused : pkgTheme.searchBox;
2332
- break;
2333
-
2334
- case "package-row":
2335
- if (props.selected) {
2336
- themeStyle = pkgTheme.selected;
2337
- } else if (props.installed) {
2338
- themeStyle = pkgTheme.installed;
2339
- } else {
2340
- themeStyle = pkgTheme.available;
2341
- }
2342
- break;
2343
-
2344
- case "detail-title":
2345
- themeStyle = pkgTheme.header;
2346
- break;
2347
-
2348
- case "detail-sep":
2349
- case "separator":
2350
- themeStyle = pkgTheme.separator;
2351
- break;
2352
-
2353
- case "divider":
2354
- themeStyle = pkgTheme.divider;
2355
- break;
2356
-
2357
- case "detail-meta":
2358
- case "detail-tags":
2359
- case "detail-url":
2360
- themeStyle = pkgTheme.infoLabel;
2361
- break;
2362
-
2363
- case "detail-desc":
2364
- themeStyle = pkgTheme.description;
2365
- break;
2366
-
2367
- case "action-btn":
2368
- themeStyle = props.focused ? pkgTheme.buttonFocused : pkgTheme.button;
2369
- break;
2370
-
2371
- case "help":
2372
- themeStyle = pkgTheme.help;
2373
- break;
2374
-
2375
- case "empty-state":
2376
- themeStyle = pkgTheme.emptyState;
2377
- break;
2378
- }
2379
-
2380
- if (themeStyle) {
2381
- const fg = themeStyle.fg;
2382
- const bg = themeStyle.bg;
2383
-
2384
- // Build overlay options - prefer theme keys, fallback to RGB
2385
- const options: Record<string, unknown> = {};
2386
-
2387
- if (fg?.theme) {
2388
- options.fg = fg.theme;
2389
- } else if (fg?.rgb) {
2390
- options.fg = fg.rgb;
2391
- }
2392
-
2393
- if (bg?.theme) {
2394
- options.bg = bg.theme;
2395
- } else if (bg?.rgb) {
2396
- options.bg = bg.rgb;
2397
- }
2398
-
2399
- if (Object.keys(options).length > 0) {
2400
- editor.addOverlay(
2401
- pkgState.bufferId,
2402
- "pkg",
2403
- byteOffset,
2404
- byteOffset + len,
2405
- options
2406
- );
2407
- }
2408
- }
2409
-
2410
- byteOffset += len;
2411
- }
2136
+ function buildPkgFooterEntries(): TextPropertyEntry[] {
2137
+ let helpText = " ↑↓ Navigate Tab Next / Search Enter ";
2138
+ if (pkgState.focus.type === "action") helpText += "Activate";
2139
+ else if (pkgState.focus.type === "filter") helpText += "Filter";
2140
+ else if (pkgState.focus.type === "sync") helpText += "Sync";
2141
+ else if (pkgState.focus.type === "search") helpText += "Search";
2142
+ else helpText += "Select";
2143
+ helpText += " Esc Close";
2144
+ return [{ text: helpText + "\n", properties: { type: "help" } }];
2412
2145
  }
2413
2146
 
2414
- /**
2415
- * Update the package manager view
2416
- */
2417
2147
  function updatePkgManagerView(): void {
2418
- if (pkgState.bufferId === null) return;
2419
-
2420
- const entries = buildListViewEntries();
2421
- editor.setVirtualBufferContent(pkgState.bufferId, entries);
2422
- applyPkgManagerHighlighting();
2148
+ if (pkgState.groupId === null) return;
2149
+ editor.setPanelContent(pkgState.groupId, "header", buildPkgHeaderEntries());
2150
+ editor.setPanelContent(pkgState.groupId, "list", buildPkgListEntries());
2151
+ editor.setPanelContent(pkgState.groupId, "detail", buildPkgDetailEntries());
2152
+ editor.setPanelContent(pkgState.groupId, "footer", buildPkgFooterEntries());
2423
2153
  }
2424
2154
 
2425
2155
  /**
@@ -2445,30 +2175,39 @@ async function openPackageManager(): Promise<void> {
2445
2175
  pkgState.focus = { type: "list" };
2446
2176
 
2447
2177
  // Build package list immediately with installed packages and cached registry
2448
- // This allows viewing/managing installed packages without waiting for network
2449
2178
  pkgState.items = buildPackageList();
2450
2179
  pkgState.isLoading = false;
2451
2180
 
2452
- // Build initial entries
2453
- const entries = buildListViewEntries();
2454
-
2455
- // Create virtual buffer
2456
- const result = await editor.createVirtualBufferInExistingSplit({
2457
- name: "*Packages*",
2458
- mode: "pkg-manager",
2459
- readOnly: true,
2460
- editingDisabled: true,
2461
- showCursors: false,
2462
- entries: entries,
2463
- splitId: pkgState.splitId!,
2464
- showLineNumbers: false,
2181
+ // Create buffer group with layout:
2182
+ // vertical: [header(fixed 4), horizontal: [list, detail], footer(fixed 1)]
2183
+ const layout = JSON.stringify({
2184
+ type: "split",
2185
+ direction: "v",
2186
+ ratio: 0.1,
2187
+ first: { type: "fixed", id: "header", height: 3 },
2188
+ // ^ 3 rows: title, search, filter bar
2189
+ second: {
2190
+ type: "split",
2191
+ direction: "v",
2192
+ ratio: 0.9,
2193
+ first: {
2194
+ type: "split",
2195
+ direction: "h",
2196
+ ratio: 0.4,
2197
+ first: { type: "scrollable", id: "list" },
2198
+ second: { type: "scrollable", id: "detail" },
2199
+ },
2200
+ second: { type: "fixed", id: "footer", height: 1 },
2201
+ },
2465
2202
  });
2466
2203
 
2467
- pkgState.bufferId = result.bufferId;
2204
+ const groupResult = await editor.createBufferGroup("*Packages*", "pkg-manager", layout);
2205
+ pkgState.groupId = groupResult.groupId;
2206
+ pkgState.panelBuffers = groupResult.panels;
2468
2207
  pkgState.isOpen = true;
2469
2208
 
2470
- // Apply initial highlighting
2471
- applyPkgManagerHighlighting();
2209
+ // Set initial content for all panels
2210
+ updatePkgManagerView();
2472
2211
 
2473
2212
  // Sync registry in background and update view when done
2474
2213
  // User can still interact with installed packages during sync
@@ -2486,8 +2225,12 @@ async function openPackageManager(): Promise<void> {
2486
2225
  function closePackageManager(): void {
2487
2226
  if (!pkgState.isOpen) return;
2488
2227
 
2489
- // Close the buffer
2490
- if (pkgState.bufferId !== null) {
2228
+ // Close the buffer group if using the new system
2229
+ if (pkgState.groupId !== null) {
2230
+ editor.closeBufferGroup(pkgState.groupId);
2231
+ pkgState.groupId = null;
2232
+ pkgState.panelBuffers = {};
2233
+ } else if (pkgState.bufferId !== null) {
2491
2234
  editor.closeBuffer(pkgState.bufferId);
2492
2235
  }
2493
2236
 
@@ -2731,6 +2474,17 @@ registerHandler("onPkgSearchConfirmed", onPkgSearchConfirmed);
2731
2474
 
2732
2475
  editor.on("prompt_confirmed", "onPkgSearchConfirmed");
2733
2476
 
2477
+ function on_pkg_resize(): void {
2478
+ if (!pkgState.isOpen) return;
2479
+ const viewport = editor.getViewport();
2480
+ if (viewport) {
2481
+ pkgState.viewportHeight = viewport.height;
2482
+ }
2483
+ updatePkgManagerView();
2484
+ }
2485
+ registerHandler("on_pkg_resize", on_pkg_resize);
2486
+ editor.on("resize", "on_pkg_resize");
2487
+
2734
2488
  // Legacy Finder-based UI (kept for backwards compatibility)
2735
2489
  const registryFinder = new Finder<[string, RegistryEntry]>(editor, {
2736
2490
  id: "pkg-registry",