package client import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strconv" ) // Client is the client for communicating with the daemon type Client struct { serverURL string } // Command represents a command sent from the client to the daemon type Command struct { Action string `json:"action"` Params map[string]string `json:"params"` } // Response represents a response from the daemon to the client type Response struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } // NewClient creates a new client func NewClient(host string, port int) *Client { return &Client{ serverURL: fmt.Sprintf("http://%s:%d", host, port), } } // CheckStatus checks if the daemon is running func (c *Client) CheckStatus() (bool, error) { resp, err := http.Get(c.serverURL + "/status") if err != nil { return false, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var response Response err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return false, err } return response.Success, nil } // TabInfo contains information about a tab type TabInfo struct { ID string `json:"id"` URL string `json:"url"` IsCurrent bool `json:"is_current"` HistoryIndex int `json:"history_index"` // Position in tab history (higher = more recent) } // ListTabs returns a list of all open tabs func (c *Client) ListTabs() ([]TabInfo, error) { resp, err := http.Get(c.serverURL + "/status") if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var response Response err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return nil, fmt.Errorf("failed to parse response: %w", err) } if !response.Success { return nil, fmt.Errorf("daemon returned error: %s", response.Error) } // Extract the data data, ok := response.Data.(map[string]interface{}) if !ok { return nil, fmt.Errorf("unexpected response format") } // Get the tabs tabsData, ok := data["tabs"].(map[string]interface{}) if !ok { return nil, fmt.Errorf("unexpected tabs format") } // Get the current tab currentTab, _ := data["current_tab"].(string) // Get the tab history tabHistoryData, ok := data["tab_history"].([]interface{}) if !ok { tabHistoryData = []interface{}{} } // Create a map of tab history indices tabHistoryIndices := make(map[string]int) for i, idInterface := range tabHistoryData { id, ok := idInterface.(string) if ok { tabHistoryIndices[id] = i } } // Convert to TabInfo tabs := make([]TabInfo, 0, len(tabsData)) for id, urlInterface := range tabsData { url, ok := urlInterface.(string) if !ok { url = "" } // Get the history index (default to -1 if not in history) historyIndex, inHistory := tabHistoryIndices[id] if !inHistory { historyIndex = -1 } tabs = append(tabs, TabInfo{ ID: id, URL: url, IsCurrent: id == currentTab, HistoryIndex: historyIndex, }) } return tabs, nil } // SendCommand sends a command to the daemon func (c *Client) SendCommand(action string, params map[string]string) (*Response, error) { cmd := Command{ Action: action, Params: params, } jsonData, err := json.Marshal(cmd) if err != nil { return nil, err } resp, err := http.Post(c.serverURL+"/command", "application/json", bytes.NewBuffer(jsonData)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, body) } var response Response err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { return nil, err } return &response, nil } // OpenTab opens a new tab // timeout is in seconds, 0 means no timeout func (c *Client) OpenTab(timeout int) (string, error) { params := map[string]string{} // Add timeout if specified if timeout > 0 { params["timeout"] = strconv.Itoa(timeout) } resp, err := c.SendCommand("open-tab", params) if err != nil { return "", err } if !resp.Success { return "", fmt.Errorf("failed to open tab: %s", resp.Error) } tabID, ok := resp.Data.(string) if !ok { return "", fmt.Errorf("unexpected response data type") } return tabID, nil } // LoadURL loads a URL in a tab // If tabID is empty, the current tab will be used // timeout is in seconds, 0 means no timeout func (c *Client) LoadURL(tabID, url string, timeout int) error { params := map[string]string{ "url": url, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeout if specified if timeout > 0 { params["timeout"] = strconv.Itoa(timeout) } resp, err := c.SendCommand("load-url", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to load URL: %s", resp.Error) } return nil } // FillFormField fills a form field with a value // If tabID is empty, the current tab will be used // selectionTimeout and actionTimeout are in seconds, 0 means no timeout func (c *Client) FillFormField(tabID, selector, value string, selectionTimeout, actionTimeout int) error { params := map[string]string{ "selector": selector, "value": value, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeouts if specified if selectionTimeout > 0 { params["selection-timeout"] = strconv.Itoa(selectionTimeout) } if actionTimeout > 0 { params["action-timeout"] = strconv.Itoa(actionTimeout) } resp, err := c.SendCommand("fill-form", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to fill form field: %s", resp.Error) } return nil } // UploadFile uploads a file to a file input // If tabID is empty, the current tab will be used // selectionTimeout and actionTimeout are in seconds, 0 means no timeout func (c *Client) UploadFile(tabID, selector, filePath string, selectionTimeout, actionTimeout int) error { params := map[string]string{ "selector": selector, "file": filePath, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeouts if specified if selectionTimeout > 0 { params["selection-timeout"] = strconv.Itoa(selectionTimeout) } if actionTimeout > 0 { params["action-timeout"] = strconv.Itoa(actionTimeout) } resp, err := c.SendCommand("upload-file", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to upload file: %s", resp.Error) } return nil } // SubmitForm submits a form // If tabID is empty, the current tab will be used // selectionTimeout and actionTimeout are in seconds, 0 means no timeout func (c *Client) SubmitForm(tabID, selector string, selectionTimeout, actionTimeout int) error { params := map[string]string{ "selector": selector, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeouts if specified if selectionTimeout > 0 { params["selection-timeout"] = strconv.Itoa(selectionTimeout) } if actionTimeout > 0 { params["action-timeout"] = strconv.Itoa(actionTimeout) } resp, err := c.SendCommand("submit-form", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to submit form: %s", resp.Error) } return nil } // GetPageSource gets the source code of a page // If tabID is empty, the current tab will be used // timeout is in seconds, 0 means no timeout func (c *Client) GetPageSource(tabID string, timeout int) (string, error) { params := map[string]string{} // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeout if specified if timeout > 0 { params["timeout"] = strconv.Itoa(timeout) } resp, err := c.SendCommand("get-source", params) if err != nil { return "", err } if !resp.Success { return "", fmt.Errorf("failed to get page source: %s", resp.Error) } source, ok := resp.Data.(string) if !ok { return "", fmt.Errorf("unexpected response data type") } return source, nil } // GetElementHTML gets the HTML of an element // If tabID is empty, the current tab will be used // selectionTimeout is in seconds, 0 means no timeout func (c *Client) GetElementHTML(tabID, selector string, selectionTimeout int) (string, error) { params := map[string]string{ "selector": selector, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeout if specified if selectionTimeout > 0 { params["selection-timeout"] = strconv.Itoa(selectionTimeout) } resp, err := c.SendCommand("get-element", params) if err != nil { return "", err } if !resp.Success { return "", fmt.Errorf("failed to get element HTML: %s", resp.Error) } html, ok := resp.Data.(string) if !ok { return "", fmt.Errorf("unexpected response data type") } return html, nil } // CloseTab closes a tab // If tabID is empty, the current tab will be used // timeout is in seconds, 0 means no timeout func (c *Client) CloseTab(tabID string, timeout int) error { params := map[string]string{} // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeout if specified if timeout > 0 { params["timeout"] = strconv.Itoa(timeout) } resp, err := c.SendCommand("close-tab", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to close tab: %s", resp.Error) } return nil } // WaitNavigation waits for a navigation event // If tabID is empty, the current tab will be used func (c *Client) WaitNavigation(tabID string, timeout int) error { params := map[string]string{ "timeout": fmt.Sprintf("%d", timeout), } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } resp, err := c.SendCommand("wait-navigation", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to wait for navigation: %s", resp.Error) } return nil } // EvalJS executes JavaScript code in a tab and returns the result // If tabID is empty, the current tab will be used // timeout is in seconds, 0 means no timeout func (c *Client) EvalJS(tabID, jsCode string, timeout int) (string, error) { params := map[string]string{ "code": jsCode, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeout if specified if timeout > 0 { params["timeout"] = strconv.Itoa(timeout) } resp, err := c.SendCommand("eval-js", params) if err != nil { return "", err } if !resp.Success { return "", fmt.Errorf("failed to execute JavaScript: %s", resp.Error) } // Convert result to string if it exists if resp.Data != nil { if result, ok := resp.Data.(string); ok { return result, nil } // If it's not a string, convert it to string representation return fmt.Sprintf("%v", resp.Data), nil } return "", nil } // TakeScreenshot takes a screenshot of a tab and saves it to a file // If tabID is empty, the current tab will be used // timeout is in seconds, 0 means no timeout func (c *Client) TakeScreenshot(tabID, outputPath string, fullPage bool, timeout int) error { params := map[string]string{ "output": outputPath, "full-page": strconv.FormatBool(fullPage), } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeout if specified if timeout > 0 { params["timeout"] = strconv.Itoa(timeout) } resp, err := c.SendCommand("screenshot", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to take screenshot: %s", resp.Error) } return nil } // SwitchToIframe switches the context to an iframe for subsequent commands // If tabID is empty, the current tab will be used func (c *Client) SwitchToIframe(tabID, selector string) error { params := map[string]string{ "selector": selector, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } resp, err := c.SendCommand("switch-iframe", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to switch to iframe: %s", resp.Error) } return nil } // SwitchToMain switches the context back to the main page // If tabID is empty, the current tab will be used func (c *Client) SwitchToMain(tabID string) error { params := map[string]string{} // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } resp, err := c.SendCommand("switch-main", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to switch to main context: %s", resp.Error) } return nil } // ClickElement clicks on an element // If tabID is empty, the current tab will be used // selectionTimeout and actionTimeout are in seconds, 0 means no timeout func (c *Client) ClickElement(tabID, selector string, selectionTimeout, actionTimeout int) error { params := map[string]string{ "selector": selector, } // Only include tab ID if it's provided if tabID != "" { params["tab"] = tabID } // Add timeouts if specified if selectionTimeout > 0 { params["selection-timeout"] = strconv.Itoa(selectionTimeout) } if actionTimeout > 0 { params["action-timeout"] = strconv.Itoa(actionTimeout) } resp, err := c.SendCommand("click-element", params) if err != nil { return err } if !resp.Success { return fmt.Errorf("failed to click element: %s", resp.Error) } return nil }