@@ -796,6 +888,7 @@ const WebView = memo(({ model, onFailLoad }: WebViewProps) => {
)}
handleWidgetSelect(widget)}
diff --git a/frontend/tailwindsetup.css b/frontend/tailwindsetup.css
index b755802d77..578567a921 100644
--- a/frontend/tailwindsetup.css
+++ b/frontend/tailwindsetup.css
@@ -23,9 +23,13 @@
--color-warning: rgb(224, 185, 86);
--color-success: rgb(78, 154, 6);
--color-panel: rgba(31, 33, 31, 0.5);
- --color-highlightbg: rgba(255, 255, 255, 0.2);
--color-hover: rgba(255, 255, 255, 0.1);
--color-border: rgba(255, 255, 255, 0.16);
+ --color-modalbg: #232323;
+ --color-accentbg: rgba(88, 193, 66, 0.5);
+ --color-hoverbg: rgba(255, 255, 255, 0.2);
+ --color-accent: rgb(88, 193, 66);
+ --color-accenthover: rgb(118, 223, 96);
--font-sans: "Inter", sans-serif;
--font-mono: "Hack", monospace;
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index eacb9e4724..f9e8be741d 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -387,7 +387,6 @@ declare global {
type FetchSuggestionsResponse = {
reqnum: number;
suggestions: SuggestionType[];
- highlightterm?: string;
};
// wshrpc.FileCopyOpts
@@ -468,6 +467,7 @@ declare global {
presets: {[key: string]: MetaType};
termthemes: {[key: string]: TermThemeType};
connections: {[key: string]: ConnKeywords};
+ bookmarks: {[key: string]: WebBookmark};
configerrors: ConfigError[];
};
@@ -581,6 +581,7 @@ declare global {
"term:conndebug"?: string;
"web:zoom"?: number;
"web:hidenav"?: boolean;
+ "web:partition"?: string;
"markdown:fontsize"?: number;
"markdown:fixedfontsize"?: number;
"vdom:*"?: boolean;
@@ -779,13 +780,18 @@ declare global {
type SuggestionType = {
type: string;
suggestionid: string;
+ display: string;
+ subtext?: string;
icon?: string;
iconcolor?: string;
+ iconsrc?: string;
+ matchpos?: number[];
+ submatchpos?: number[];
+ score?: number;
"file:mimetype"?: string;
- "file:name"?: string;
"file:path"?: string;
- matchpositions?: number[];
- score?: number;
+ "file:name"?: string;
+ "url:url"?: string;
};
// telemetrydata.TEvent
@@ -1281,6 +1287,16 @@ declare global {
lastfocusts: number;
};
+ // wconfig.WebBookmark
+ type WebBookmark = {
+ url: string;
+ title?: string;
+ icon?: string;
+ iconcolor?: string;
+ iconurl?: string;
+ "display:order"?: number;
+ };
+
// service.WebCallType
type WebCallType = {
service: string;
diff --git a/go.mod b/go.mod
index ba0d7c2b91..504a8284d0 100644
--- a/go.mod
+++ b/go.mod
@@ -94,8 +94,8 @@ require (
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
- golang.org/x/sync v0.10.0 // indirect
- golang.org/x/text v0.21.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
diff --git a/go.sum b/go.sum
index 6d0c813ff6..3386d44c68 100644
--- a/go.sum
+++ b/go.sum
@@ -206,8 +206,8 @@ golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -220,8 +220,8 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/pkg/faviconcache/faviconcache.go b/pkg/faviconcache/faviconcache.go
new file mode 100644
index 0000000000..3fa12dbee8
--- /dev/null
+++ b/pkg/faviconcache/faviconcache.go
@@ -0,0 +1,196 @@
+// Copyright 2025, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+package faviconcache
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/wavetermdev/waveterm/pkg/panichandler"
+)
+
+// --- Constants and Types ---
+
+// cacheDuration is how long a cached entry is considered “fresh.”
+const cacheDuration = 24 * time.Hour
+
+// maxIconSize limits the favicon size to 256 KB.
+const maxIconSize = 256 * 1024 // in bytes
+
+// FaviconCacheItem represents one cached favicon entry.
+type FaviconCacheItem struct {
+ // Data is the base64-encoded data URL string (e.g. "data:image/png;base64,...")
+ Data string
+ // LastFetched is when this entry was last updated.
+ LastFetched time.Time
+}
+
+// --- Global variables for managing in-flight fetches ---
+// We use a mutex and a simple map to prevent multiple simultaneous fetches for the same domain.
+var (
+ fetchLock sync.Mutex
+ fetching = make(map[string]bool)
+)
+
+// Use a semaphore (buffered channel) to limit concurrent fetches to 5.
+var fetchSemaphore = make(chan bool, 5)
+
+var (
+ faviconCacheLock sync.Mutex
+ faviconCache = make(map[string]*FaviconCacheItem)
+)
+
+// --- GetFavicon ---
+//
+// GetFavicon takes a URL string and returns a base64-encoded src URL for an
![]()
+// tag. If the favicon is already in cache and “fresh,” it returns it immediately.
+// Otherwise it kicks off a background fetch (if one isn’t already in progress)
+// and returns whatever is in the cache (which may be empty).
+func GetFavicon(urlStr string) string {
+ // Parse the URL and extract the domain.
+ parsedURL, err := url.Parse(urlStr)
+ if err != nil {
+ log.Printf("GetFavicon: invalid URL %q: %v", urlStr, err)
+ return ""
+ }
+ domain := parsedURL.Hostname()
+ if domain == "" {
+ log.Printf("GetFavicon: no hostname found in URL %q", urlStr)
+ return ""
+ }
+
+ // Try to get from our cache.
+ item, found := GetFromCache(domain)
+ if found {
+ // If the cached entry is not stale, return it.
+ if time.Since(item.LastFetched) < cacheDuration {
+ return item.Data
+ }
+ }
+
+ // Either the item was not found or it’s stale:
+ // Launch an async fetch if one isn’t already running for this domain.
+ triggerAsyncFetch(domain)
+
+ // Return the cached value (even if stale or empty).
+ return item.Data
+}
+
+// triggerAsyncFetch starts a goroutine to update the favicon cache
+// for the given domain if one isn’t already in progress.
+func triggerAsyncFetch(domain string) {
+ fetchLock.Lock()
+ if fetching[domain] {
+ // Already fetching this domain; nothing to do.
+ fetchLock.Unlock()
+ return
+ }
+ // Mark this domain as in-flight.
+ fetching[domain] = true
+ fetchLock.Unlock()
+
+ go func() {
+ defer func() {
+ panichandler.PanicHandler("Favicon:triggerAsyncFetch", recover())
+ }()
+
+ // Acquire a slot in the semaphore.
+ fetchSemaphore <- true
+
+ // When done, ensure that we clear the “fetching” flag.
+ defer func() {
+ <-fetchSemaphore
+ fetchLock.Lock()
+ delete(fetching, domain)
+ fetchLock.Unlock()
+ }()
+
+ iconStr, err := fetchFavicon(domain)
+ if err != nil {
+ log.Printf("triggerAsyncFetch: error fetching favicon for %s: %v", domain, err)
+ }
+ SetInCache(domain, FaviconCacheItem{Data: iconStr, LastFetched: time.Now()})
+ }()
+}
+
+func fetchFavicon(domain string) (string, error) {
+ // Create a context that times out after 5 seconds.
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ // Special case for github.com - use their dark favicon from assets domain
+ url := "https://" + domain + "/favicon.ico"
+ if domain == "github.com" {
+ url = "https://github.githubassets.com/favicons/favicon-dark.png"
+ }
+
+ // Create a new HTTP request with the context.
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ return "", fmt.Errorf("error creating request for %s: %w", url, err)
+ }
+
+ // Execute the HTTP request.
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("error fetching favicon from %s: %w", url, err)
+ }
+ defer resp.Body.Close()
+
+ // Ensure we got a 200 OK.
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("non-OK HTTP status: %d fetching %s", resp.StatusCode, url)
+ }
+
+ // Read the favicon bytes.
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("error reading favicon data from %s: %w", url, err)
+ }
+
+ // Encode the image bytes to base64.
+ b64Data := base64.StdEncoding.EncodeToString(data)
+ if len(b64Data) > maxIconSize {
+ return "", fmt.Errorf("favicon too large: %d bytes", len(b64Data))
+ }
+
+ // Try to detect MIME type from Content-Type header first
+ mimeType := resp.Header.Get("Content-Type")
+ if mimeType == "" {
+ // If no Content-Type header, detect from content
+ mimeType = http.DetectContentType(data)
+ }
+
+ if !strings.HasPrefix(mimeType, "image/") {
+ return "", fmt.Errorf("unexpected MIME type: %s", mimeType)
+ }
+
+ return "data:" + mimeType + ";base64," + b64Data, nil
+}
+
+// TODO store in blockstore
+
+func GetFromCache(key string) (FaviconCacheItem, bool) {
+ faviconCacheLock.Lock()
+ defer faviconCacheLock.Unlock()
+ item, found := faviconCache[key]
+ if !found {
+ return FaviconCacheItem{}, false
+ }
+ return *item, true
+}
+
+func SetInCache(key string, item FaviconCacheItem) {
+ faviconCacheLock.Lock()
+ defer faviconCacheLock.Unlock()
+ faviconCache[key] = &item
+}
diff --git a/pkg/suggestion/suggestion.go b/pkg/suggestion/suggestion.go
index 0af5235e6d..133ffc1da8 100644
--- a/pkg/suggestion/suggestion.go
+++ b/pkg/suggestion/suggestion.go
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"io/fs"
+ "net/url"
"os"
"path/filepath"
"sort"
@@ -14,12 +15,16 @@ import (
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
+ "github.com/wavetermdev/waveterm/pkg/faviconcache"
"github.com/wavetermdev/waveterm/pkg/util/fileutil"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/wavebase"
+ "github.com/wavetermdev/waveterm/pkg/wconfig"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
)
+const MaxSuggestions = 50
+
type MockDirEntry struct {
NameStr string
IsDirVal bool
@@ -126,8 +131,199 @@ func resolveFileQuery(cwd string, query string) (string, string, string, error)
return cwd, "", query, nil
}
-// FetchSuggestions returns file suggestions using junegunn/fzf’s fuzzy matching.
func FetchSuggestions(ctx context.Context, data wshrpc.FetchSuggestionsData) (*wshrpc.FetchSuggestionsResponse, error) {
+ if data.SuggestionType == "file" {
+ return fetchFileSuggestions(ctx, data)
+ }
+ if data.SuggestionType == "bookmark" {
+ return fetchBookmarkSuggestions(ctx, data)
+ }
+ return nil, fmt.Errorf("unsupported suggestion type: %q", data.SuggestionType)
+}
+
+func filterBookmarksForValid(bookmarks map[string]wconfig.WebBookmark) map[string]wconfig.WebBookmark {
+ validBookmarks := make(map[string]wconfig.WebBookmark)
+ for k, v := range bookmarks {
+ if v.Url == "" {
+ continue
+ }
+ u, err := url.ParseRequestURI(v.Url)
+ if err != nil || u.Scheme == "" || u.Host == "" {
+ continue
+ }
+
+ validBookmarks[k] = v
+ }
+ return validBookmarks
+}
+
+func fetchBookmarkSuggestions(_ context.Context, data wshrpc.FetchSuggestionsData) (*wshrpc.FetchSuggestionsResponse, error) {
+ if data.SuggestionType != "bookmark" {
+ return nil, fmt.Errorf("unsupported suggestion type: %q", data.SuggestionType)
+ }
+
+ // scoredEntry holds a bookmark along with its computed score, the match positions for the
+ // field that will be used for display, the positions for the secondary field (if any),
+ // and its original index in the Bookmarks list.
+ type scoredEntry struct {
+ bookmark wconfig.WebBookmark
+ score int
+ matchPos []int // positions for the field that's used as Display
+ subMatchPos []int // positions for the other field (if any)
+ origIndex int
+ }
+
+ bookmarks := wconfig.GetWatcher().GetFullConfig().Bookmarks
+ bookmarks = filterBookmarksForValid(bookmarks)
+
+ searchTerm := data.Query
+ var patternRunes []rune
+ if searchTerm != "" {
+ patternRunes = []rune(strings.ToLower(searchTerm))
+ }
+
+ var scoredEntries []scoredEntry
+ var slab util.Slab
+
+ bookmarkKeys := utilfn.GetMapKeys(bookmarks)
+ // sort by display:order and then by key
+ sort.Slice(bookmarkKeys, func(i, j int) bool {
+ bookmarkA := bookmarks[bookmarkKeys[i]]
+ bookmarkB := bookmarks[bookmarkKeys[j]]
+ if bookmarkA.DisplayOrder != bookmarkB.DisplayOrder {
+ return bookmarkA.DisplayOrder < bookmarkB.DisplayOrder
+ }
+ return bookmarkKeys[i] < bookmarkKeys[j]
+ })
+ for i, bmkey := range bookmarkKeys {
+ bookmark := bookmarks[bmkey]
+ // If no search term, include all bookmarks (score 0, no positions).
+ if searchTerm == "" {
+ scoredEntries = append(scoredEntries, scoredEntry{
+ bookmark: bookmark,
+ score: 0,
+ origIndex: i,
+ })
+ continue
+ }
+
+ // For bookmarks with a title, Display is set to the title and SubText to the URL.
+ // We perform fuzzy matching on both fields.
+ if bookmark.Title != "" {
+ // Fuzzy match against the title.
+ candidateTitle := strings.ToLower(bookmark.Title)
+ textTitle := util.ToChars([]byte(candidateTitle))
+ resultTitle, titlePositionsPtr := algo.FuzzyMatchV2(false, true, true, &textTitle, patternRunes, true, &slab)
+ var titleScore int
+ var titlePositions []int
+ if titlePositionsPtr != nil {
+ titlePositions = *titlePositionsPtr
+ }
+ titleScore = resultTitle.Score
+
+ // Fuzzy match against the URL.
+ candidateUrl := strings.ToLower(bookmark.Url)
+ textUrl := util.ToChars([]byte(candidateUrl))
+ resultUrl, urlPositionsPtr := algo.FuzzyMatchV2(false, true, true, &textUrl, patternRunes, true, &slab)
+ var urlScore int
+ var urlPositions []int
+ if urlPositionsPtr != nil {
+ urlPositions = *urlPositionsPtr
+ }
+ urlScore = resultUrl.Score
+
+ // Compute the overall score as the higher of the two.
+ maxScore := titleScore
+ if urlScore > maxScore {
+ maxScore = urlScore
+ }
+
+ // If neither field produced a positive match, skip this bookmark.
+ if maxScore <= 0 {
+ continue
+ }
+
+ // Since Display is title, we use the title match positions as MatchPos and the URL match positions as SubMatchPos.
+ scoredEntries = append(scoredEntries, scoredEntry{
+ bookmark: bookmark,
+ score: maxScore,
+ matchPos: titlePositions,
+ subMatchPos: urlPositions,
+ origIndex: i,
+ })
+ } else {
+ // For bookmarks with no title, Display is set to the URL.
+ // Only perform fuzzy matching against the URL.
+ candidateUrl := strings.ToLower(bookmark.Url)
+ textUrl := util.ToChars([]byte(candidateUrl))
+ resultUrl, urlPositionsPtr := algo.FuzzyMatchV2(false, true, true, &textUrl, patternRunes, true, &slab)
+ urlScore := resultUrl.Score
+ var urlPositions []int
+ if urlPositionsPtr != nil {
+ urlPositions = *urlPositionsPtr
+ }
+
+ // Skip this bookmark if the URL doesn't match.
+ if urlScore <= 0 {
+ continue
+ }
+
+ scoredEntries = append(scoredEntries, scoredEntry{
+ bookmark: bookmark,
+ score: urlScore,
+ matchPos: urlPositions, // match positions come from the URL, since that's what is displayed.
+ subMatchPos: nil,
+ origIndex: i,
+ })
+ }
+ }
+
+ // Sort the scored entries in descending order by score.
+ // For equal scores, preserve the original order from the Bookmarks list.
+ sort.Slice(scoredEntries, func(i, j int) bool {
+ if scoredEntries[i].score != scoredEntries[j].score {
+ return scoredEntries[i].score > scoredEntries[j].score
+ }
+ return scoredEntries[i].origIndex < scoredEntries[j].origIndex
+ })
+
+ // Build up to MaxSuggestions suggestions.
+ var suggestions []wshrpc.SuggestionType
+ for _, entry := range scoredEntries {
+ var display, subText string
+ if entry.bookmark.Title != "" {
+ display = entry.bookmark.Title
+ subText = entry.bookmark.Url
+ } else {
+ display = entry.bookmark.Url
+ subText = ""
+ }
+
+ suggestion := wshrpc.SuggestionType{
+ Type: "url",
+ SuggestionId: utilfn.QuickHashString(entry.bookmark.Url),
+ Display: display,
+ SubText: subText,
+ MatchPos: entry.matchPos, // These positions correspond to the field in Display.
+ SubMatchPos: entry.subMatchPos, // For bookmarks with a title, this is the URL match positions.
+ Score: entry.score,
+ UrlUrl: entry.bookmark.Url,
+ }
+ suggestion.IconSrc = faviconcache.GetFavicon(entry.bookmark.Url)
+ suggestions = append(suggestions, suggestion)
+ if len(suggestions) >= MaxSuggestions {
+ break
+ }
+ }
+
+ return &wshrpc.FetchSuggestionsResponse{
+ Suggestions: suggestions,
+ ReqNum: data.ReqNum,
+ }, nil
+}
+
+// FetchSuggestions returns file suggestions using junegunn/fzf’s fuzzy matching.
+func fetchFileSuggestions(_ context.Context, data wshrpc.FetchSuggestionsData) (*wshrpc.FetchSuggestionsResponse, error) {
// Only support file suggestions.
if data.SuggestionType != "file" {
return nil, fmt.Errorf("unsupported suggestion type: %q", data.SuggestionType)
@@ -222,12 +418,9 @@ func FetchSuggestions(ctx context.Context, data wshrpc.FetchSuggestionsData) (*w
})
}
- // Build up to 50 suggestions.
+ // Build up to MaxSuggestions suggestions
var suggestions []wshrpc.SuggestionType
- for i, candidate := range scoredEntries {
- if i >= 50 {
- break
- }
+ for _, candidate := range scoredEntries {
fileName := candidate.ent.Name()
fullPath := filepath.Join(baseDir, fileName)
suggestionFileName := filepath.Join(queryPrefix, fileName)
@@ -242,18 +435,20 @@ func FetchSuggestions(ctx context.Context, data wshrpc.FetchSuggestionsData) (*w
Type: "file",
FilePath: fullPath,
SuggestionId: utilfn.QuickHashString(fullPath),
- // Use the queryPrefix to build the display name.
- FileName: suggestionFileName,
- FileMimeType: fileutil.DetectMimeTypeWithDirEnt(fullPath, candidate.ent),
- MatchPositions: scoredEntries[i].positions,
- Score: candidate.score,
+ Display: suggestionFileName,
+ FileName: suggestionFileName,
+ FileMimeType: fileutil.DetectMimeTypeWithDirEnt(fullPath, candidate.ent),
+ MatchPos: candidate.positions,
+ Score: candidate.score,
}
suggestions = append(suggestions, s)
+ if len(suggestions) >= MaxSuggestions {
+ break
+ }
}
return &wshrpc.FetchSuggestionsResponse{
- Suggestions: suggestions,
- ReqNum: data.ReqNum,
- HighlightTerm: searchTerm,
+ Suggestions: suggestions,
+ ReqNum: data.ReqNum,
}, nil
}
diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go
index c6fec36d57..a3365eb8b1 100644
--- a/pkg/waveobj/metaconsts.go
+++ b/pkg/waveobj/metaconsts.go
@@ -106,6 +106,7 @@ const (
MetaKey_WebZoom = "web:zoom"
MetaKey_WebHideNav = "web:hidenav"
+ MetaKey_WebPartition = "web:partition"
MetaKey_MarkdownFontSize = "markdown:fontsize"
MetaKey_MarkdownFixedFontSize = "markdown:fixedfontsize"
diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go
index 6705aa4fd8..d583cc90c9 100644
--- a/pkg/waveobj/wtypemeta.go
+++ b/pkg/waveobj/wtypemeta.go
@@ -107,8 +107,9 @@ type MetaTSType struct {
TermAllowBracketedPaste *bool `json:"term:allowbracketedpaste,omitempty"`
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
- WebZoom float64 `json:"web:zoom,omitempty"`
- WebHideNav *bool `json:"web:hidenav,omitempty"`
+ WebZoom float64 `json:"web:zoom,omitempty"`
+ WebHideNav *bool `json:"web:hidenav,omitempty"`
+ WebPartition string `json:"web:partition,omitempty"`
MarkdownFontSize float64 `json:"markdown:fontsize,omitempty"`
MarkdownFixedFontSize float64 `json:"markdown:fixedfontsize,omitempty"`
diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go
index bb85899188..a1829d233c 100644
--- a/pkg/wconfig/settingsconfig.go
+++ b/pkg/wconfig/settingsconfig.go
@@ -125,6 +125,15 @@ type ConfigError struct {
Err string `json:"err"`
}
+type WebBookmark struct {
+ Url string `json:"url"`
+ Title string `json:"title,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ IconColor string `json:"iconcolor,omitempty"`
+ IconUrl string `json:"iconurl,omitempty"`
+ DisplayOrder float64 `json:"display:order,omitempty"`
+}
+
type FullConfigType struct {
Settings SettingsType `json:"settings" merge:"meta"`
MimeTypes map[string]MimeTypeConfigType `json:"mimetypes"`
@@ -133,6 +142,7 @@ type FullConfigType struct {
Presets map[string]waveobj.MetaMapType `json:"presets"`
TermThemes map[string]TermThemeType `json:"termthemes"`
Connections map[string]ConnKeywords `json:"connections"`
+ Bookmarks map[string]WebBookmark `json:"bookmarks"`
ConfigErrors []ConfigError `json:"configerrors" configfile:"-"`
}
type ConnKeywords struct {
diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go
index a9c5137d8b..2a74a9c63e 100644
--- a/pkg/wshrpc/wshrpctypes.go
+++ b/pkg/wshrpc/wshrpctypes.go
@@ -733,19 +733,23 @@ type FetchSuggestionsData struct {
}
type FetchSuggestionsResponse struct {
- ReqNum int `json:"reqnum"`
- Suggestions []SuggestionType `json:"suggestions"`
- HighlightTerm string `json:"highlightterm,omitempty"`
+ ReqNum int `json:"reqnum"`
+ Suggestions []SuggestionType `json:"suggestions"`
}
type SuggestionType struct {
- Type string `json:"type"`
- SuggestionId string `json:"suggestionid"`
- Icon string `json:"icon,omitempty"`
- IconColor string `json:"iconcolor,omitempty"`
- FileMimeType string `json:"file:mimetype,omitempty"`
- FileName string `json:"file:name,omitempty"`
- FilePath string `json:"file:path,omitempty"`
- MatchPositions []int `json:"matchpositions,omitempty"`
- Score int `json:"score,omitempty"`
+ Type string `json:"type"`
+ SuggestionId string `json:"suggestionid"`
+ Display string `json:"display"`
+ SubText string `json:"subtext,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ IconColor string `json:"iconcolor,omitempty"`
+ IconSrc string `json:"iconsrc,omitempty"`
+ MatchPos []int `json:"matchpos,omitempty"`
+ SubMatchPos []int `json:"submatchpos,omitempty"`
+ Score int `json:"score,omitempty"`
+ FileMimeType string `json:"file:mimetype,omitempty"`
+ FilePath string `json:"file:path,omitempty"`
+ FileName string `json:"file:name,omitempty"`
+ UrlUrl string `json:"url:url,omitempty"`
}
diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go
index c8e9d95bb1..45426cbdd2 100644
--- a/pkg/wshrpc/wshserver/wshserver.go
+++ b/pkg/wshrpc/wshserver/wshserver.go
@@ -26,6 +26,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/remote"
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare"
+ "github.com/wavetermdev/waveterm/pkg/suggestion"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
"github.com/wavetermdev/waveterm/pkg/util/envutil"
@@ -861,3 +862,7 @@ func (ws *WshServer) PathCommand(ctx context.Context, data wshrpc.PathCommandDat
}
return path, nil
}
+
+func (ws *WshServer) FetchSuggestionsCommand(ctx context.Context, data wshrpc.FetchSuggestionsData) (*wshrpc.FetchSuggestionsResponse, error) {
+ return suggestion.FetchSuggestions(ctx, data)
+}