initial commit
This commit is contained in:
627
internal/config/deployer.go
Normal file
627
internal/config/deployer.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
type ConfigDeployer struct {
|
||||
logChan chan<- string
|
||||
}
|
||||
|
||||
type DeploymentResult struct {
|
||||
ConfigType string
|
||||
Path string
|
||||
BackupPath string
|
||||
Deployed bool
|
||||
Error error
|
||||
}
|
||||
|
||||
func NewConfigDeployer(logChan chan<- string) *ConfigDeployer {
|
||||
return &ConfigDeployer{
|
||||
logChan: logChan,
|
||||
}
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) log(message string) {
|
||||
if cd.logChan != nil {
|
||||
cd.logChan <- message
|
||||
}
|
||||
}
|
||||
|
||||
// DeployConfigurations deploys all necessary configurations based on the chosen window manager
|
||||
func (cd *ConfigDeployer) DeployConfigurations(ctx context.Context, wm deps.WindowManager) ([]DeploymentResult, error) {
|
||||
return cd.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
// DeployConfigurationsWithTerminal deploys all necessary configurations based on chosen window manager and terminal
|
||||
func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]DeploymentResult, error) {
|
||||
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
|
||||
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
shouldReplaceConfig := func(configType string) bool {
|
||||
if replaceConfigs == nil {
|
||||
return true
|
||||
}
|
||||
replace, exists := replaceConfigs[configType]
|
||||
return !exists || replace
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
if shouldReplaceConfig("Niri") {
|
||||
result, err := cd.deployNiriConfig(terminal)
|
||||
results = append(results, result)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.WindowManagerHyprland:
|
||||
if shouldReplaceConfig("Hyprland") {
|
||||
result, err := cd.deployHyprlandConfig(terminal)
|
||||
results = append(results, result)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
if shouldReplaceConfig("Ghostty") {
|
||||
ghosttyResults, err := cd.deployGhosttyConfig()
|
||||
results = append(results, ghosttyResults...)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Ghostty config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.TerminalKitty:
|
||||
if shouldReplaceConfig("Kitty") {
|
||||
kittyResults, err := cd.deployKittyConfig()
|
||||
results = append(results, kittyResults...)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Kitty config: %w", err)
|
||||
}
|
||||
}
|
||||
case deps.TerminalAlacritty:
|
||||
if shouldReplaceConfig("Alacritty") {
|
||||
alacrittyResults, err := cd.deployAlacrittyConfig()
|
||||
results = append(results, alacrittyResults...)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy Alacritty config: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if zsh was installed and deploy .zshrc configuration
|
||||
if installedDeps != nil {
|
||||
for _, dep := range installedDeps {
|
||||
if dep.Name == "zsh" && (dep.Status == deps.StatusInstalled || dep.Status == deps.StatusNeedsUpdate) {
|
||||
if shouldReplaceConfig(".zshrc") {
|
||||
zshResult, err := cd.deployZshrcConfig()
|
||||
results = append(results, zshResult)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("failed to deploy .zshrc config: %w", err)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// deployNiriConfig handles Niri configuration deployment with backup and merging
|
||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
ConfigType: "Niri",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(result.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
var existingConfig string
|
||||
if _, err := os.Stat(result.Path); err == nil {
|
||||
cd.log("Found existing Niri configuration")
|
||||
|
||||
existingData, err := os.ReadFile(result.Path)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
existingConfig = string(existingData)
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
result.BackupPath = result.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||
}
|
||||
|
||||
// Detect polkit agent path
|
||||
polkitPath, err := cd.detectPolkitAgent()
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
||||
}
|
||||
|
||||
// Determine terminal command based on choice
|
||||
var terminalCommand string
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
terminalCommand = "ghostty"
|
||||
case deps.TerminalKitty:
|
||||
terminalCommand = "kitty"
|
||||
case deps.TerminalAlacritty:
|
||||
terminalCommand = "alacritty"
|
||||
default:
|
||||
terminalCommand = "ghostty" // fallback to ghostty
|
||||
}
|
||||
|
||||
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
|
||||
// If there was an existing config, merge the output sections
|
||||
if existingConfig != "" {
|
||||
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
||||
} else {
|
||||
newConfig = mergedConfig
|
||||
cd.log("Successfully merged existing output sections")
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.Deployed = true
|
||||
cd.log("Successfully deployed Niri configuration")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
mainResult := DeploymentResult{
|
||||
ConfigType: "Ghostty",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(mainResult.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||
cd.log("Found existing Ghostty configuration")
|
||||
|
||||
existingData, err := os.ReadFile(mainResult.Path)
|
||||
if err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
mainResult.Deployed = true
|
||||
cd.log("Successfully deployed Ghostty configuration")
|
||||
results = append(results, mainResult)
|
||||
|
||||
colorResult := DeploymentResult{
|
||||
ConfigType: "Ghostty Colors",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
||||
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
||||
return results, colorResult.Error
|
||||
}
|
||||
|
||||
colorResult.Deployed = true
|
||||
cd.log("Successfully deployed Ghostty color configuration")
|
||||
results = append(results, colorResult)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
mainResult := DeploymentResult{
|
||||
ConfigType: "Kitty",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "kitty.conf"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(mainResult.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||
cd.log("Found existing Kitty configuration")
|
||||
|
||||
existingData, err := os.ReadFile(mainResult.Path)
|
||||
if err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
mainResult.Deployed = true
|
||||
cd.log("Successfully deployed Kitty configuration")
|
||||
results = append(results, mainResult)
|
||||
|
||||
themeResult := DeploymentResult{
|
||||
ConfigType: "Kitty Theme",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
|
||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||
return results, themeResult.Error
|
||||
}
|
||||
|
||||
themeResult.Deployed = true
|
||||
cd.log("Successfully deployed Kitty theme configuration")
|
||||
results = append(results, themeResult)
|
||||
|
||||
tabsResult := DeploymentResult{
|
||||
ConfigType: "Kitty Tabs",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
|
||||
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
||||
return results, tabsResult.Error
|
||||
}
|
||||
|
||||
tabsResult.Deployed = true
|
||||
cd.log("Successfully deployed Kitty tabs configuration")
|
||||
results = append(results, tabsResult)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
||||
var results []DeploymentResult
|
||||
|
||||
mainResult := DeploymentResult{
|
||||
ConfigType: "Alacritty",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "alacritty.toml"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(mainResult.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mainResult.Path); err == nil {
|
||||
cd.log("Found existing Alacritty configuration")
|
||||
|
||||
existingData, err := os.ReadFile(mainResult.Path)
|
||||
if err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
|
||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
|
||||
mainResult.Deployed = true
|
||||
cd.log("Successfully deployed Alacritty configuration")
|
||||
results = append(results, mainResult)
|
||||
|
||||
themeResult := DeploymentResult{
|
||||
ConfigType: "Alacritty Theme",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
||||
}
|
||||
|
||||
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
|
||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||
return results, themeResult.Error
|
||||
}
|
||||
|
||||
themeResult.Deployed = true
|
||||
cd.log("Successfully deployed Alacritty theme configuration")
|
||||
results = append(results, themeResult)
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// detectPolkitAgent tries to find the polkit authentication agent on the system
|
||||
// Prioritizes mate-polkit paths since that's what we install
|
||||
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
|
||||
// Prioritize mate-polkit paths first
|
||||
matePaths := []string{
|
||||
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
|
||||
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
|
||||
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
|
||||
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
|
||||
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
|
||||
}
|
||||
|
||||
for _, path := range matePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to other polkit agents if mate-polkit is not found
|
||||
fallbackPaths := []string{
|
||||
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
||||
"/usr/libexec/polkit-gnome-authentication-agent-1",
|
||||
}
|
||||
|
||||
for _, path := range fallbackPaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no polkit agent found in common locations")
|
||||
}
|
||||
|
||||
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
||||
// Regular expression to match output sections (including commented ones)
|
||||
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
|
||||
// Find all output sections in the existing config
|
||||
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingOutputs) == 0 {
|
||||
// No output sections to merge
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
// Remove the example output section from the new config
|
||||
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
||||
|
||||
// Find where to insert the output sections (after the input section)
|
||||
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
||||
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
||||
|
||||
if len(inputMatches) < 1 {
|
||||
return "", fmt.Errorf("could not find insertion point for output sections")
|
||||
}
|
||||
|
||||
// Insert after the first closing brace (end of input section)
|
||||
insertPos := inputMatches[0][1]
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(mergedConfig[:insertPos])
|
||||
builder.WriteString("\n// Outputs from existing configuration\n")
|
||||
|
||||
for _, output := range existingOutputs {
|
||||
builder.WriteString(output)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
builder.WriteString(mergedConfig[insertPos:])
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
ConfigType: "Hyprland",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(result.Path)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
var existingConfig string
|
||||
if _, err := os.Stat(result.Path); err == nil {
|
||||
cd.log("Found existing Hyprland configuration")
|
||||
|
||||
existingData, err := os.ReadFile(result.Path)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to read existing config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
existingConfig = string(existingData)
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
result.BackupPath = result.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||
}
|
||||
|
||||
// Detect polkit agent path
|
||||
polkitPath, err := cd.detectPolkitAgent()
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
||||
}
|
||||
|
||||
// Determine terminal command based on choice
|
||||
var terminalCommand string
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
terminalCommand = "ghostty"
|
||||
case deps.TerminalKitty:
|
||||
terminalCommand = "kitty"
|
||||
case deps.TerminalAlacritty:
|
||||
terminalCommand = "alacritty"
|
||||
default:
|
||||
terminalCommand = "ghostty" // fallback to ghostty
|
||||
}
|
||||
|
||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
|
||||
// If there was an existing config, merge the monitor sections
|
||||
if existingConfig != "" {
|
||||
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
||||
if err != nil {
|
||||
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
||||
} else {
|
||||
newConfig = mergedConfig
|
||||
cd.log("Successfully merged existing monitor sections")
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.Deployed = true
|
||||
cd.log("Successfully deployed Hyprland configuration")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
||||
// Regular expression to match monitor lines (including commented ones)
|
||||
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
|
||||
// Also matches commented versions: # monitor = ...
|
||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||
|
||||
// Find all monitor lines in the existing config
|
||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||
|
||||
if len(existingMonitors) == 0 {
|
||||
// No monitor sections to merge
|
||||
return newConfig, nil
|
||||
}
|
||||
|
||||
// Remove the example monitor line from the new config
|
||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||
|
||||
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
|
||||
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
||||
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
||||
|
||||
if headerMatch == nil {
|
||||
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
||||
}
|
||||
|
||||
// Insert after the header
|
||||
insertPos := headerMatch[1] + 1 // +1 for the newline
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString(mergedConfig[:insertPos])
|
||||
builder.WriteString("# Monitors from existing configuration\n")
|
||||
|
||||
for _, monitor := range existingMonitors {
|
||||
builder.WriteString(monitor)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
builder.WriteString(mergedConfig[insertPos:])
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// deployZshrcConfig handles .zshrc configuration deployment with backup
|
||||
func (cd *ConfigDeployer) deployZshrcConfig() (DeploymentResult, error) {
|
||||
result := DeploymentResult{
|
||||
ConfigType: ".zshrc",
|
||||
Path: filepath.Join(os.Getenv("HOME"), ".zshrc"),
|
||||
}
|
||||
|
||||
// Check if .zshrc already exists
|
||||
if _, err := os.Stat(result.Path); err == nil {
|
||||
cd.log("Found existing .zshrc configuration")
|
||||
|
||||
existingData, err := os.ReadFile(result.Path)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to read existing .zshrc: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||
result.BackupPath = result.Path + ".backup." + timestamp
|
||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
cd.log(fmt.Sprintf("Backed up existing .zshrc to %s", result.BackupPath))
|
||||
}
|
||||
|
||||
// Write the new .zshrc configuration
|
||||
if err := os.WriteFile(result.Path, []byte(ZshrcConfig), 0644); err != nil {
|
||||
result.Error = fmt.Errorf("failed to write .zshrc: %w", err)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
result.Deployed = true
|
||||
cd.log("Successfully deployed .zshrc configuration")
|
||||
return result, nil
|
||||
}
|
||||
660
internal/config/deployer_test.go
Normal file
660
internal/config/deployer_test.go
Normal file
@@ -0,0 +1,660 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDetectPolkitAgent(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
// This test depends on the system having a polkit agent installed
|
||||
// We'll just test that the function doesn't crash and returns some path or error
|
||||
path, err := cd.detectPolkitAgent()
|
||||
|
||||
if err != nil {
|
||||
// If no polkit agent is found, that's okay for testing
|
||||
assert.Contains(t, err.Error(), "no polkit agent found")
|
||||
} else {
|
||||
// If found, it should be a valid path
|
||||
assert.NotEmpty(t, path)
|
||||
assert.True(t, strings.Contains(path, "polkit"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNiriOutputSections(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newConfig string
|
||||
existingConfig string
|
||||
wantError bool
|
||||
wantContains []string
|
||||
}{
|
||||
{
|
||||
name: "no existing outputs",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{"gaps 5"}, // Should keep new config
|
||||
},
|
||||
{
|
||||
name: "merge single output",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
output "eDP-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=0 y=0
|
||||
scale 1.0
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"gaps 5", // New config preserved
|
||||
`output "eDP-1"`, // Existing output merged
|
||||
"1920x1080@60.000000", // Existing output details
|
||||
"Outputs from existing configuration", // Comment added
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge multiple outputs",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
output "eDP-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=0 y=0
|
||||
scale 1.0
|
||||
}
|
||||
/-output "HDMI-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=1920 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"gaps 5", // New config preserved
|
||||
`output "eDP-1"`, // First existing output
|
||||
`/-output "HDMI-1"`, // Second existing output (commented)
|
||||
"1920x1080@60.000000", // Output details
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge commented outputs",
|
||||
newConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
}
|
||||
layout {
|
||||
gaps 5
|
||||
}`,
|
||||
existingConfig: `input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
}
|
||||
}
|
||||
/-output "eDP-1" {
|
||||
mode "1920x1080@60.000000"
|
||||
position x=0 y=0
|
||||
scale 1.0
|
||||
}
|
||||
layout {
|
||||
gaps 10
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"gaps 5", // New config preserved
|
||||
`/-output "eDP-1"`, // Commented output preserved
|
||||
"1920x1080@60.000000", // Output details
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||
}
|
||||
|
||||
assert.NotContains(t, result, `/-output "eDP-2"`, "example output should be removed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigDeploymentFlow(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy ghostty config to empty directory", func(t *testing.T) {
|
||||
results, err := cd.deployGhosttyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Ghostty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.Empty(t, mainResult.BackupPath)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
|
||||
content, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "window-decoration = false")
|
||||
|
||||
colorResult := results[1]
|
||||
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
|
||||
assert.True(t, colorResult.Deployed)
|
||||
assert.FileExists(t, colorResult.Path)
|
||||
|
||||
colorContent, err := os.ReadFile(colorResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(colorContent), "background = #101418")
|
||||
})
|
||||
|
||||
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
||||
existingContent := "# Old config\nfont-size = 14\n"
|
||||
ghosttyPath := getGhosttyPath()
|
||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := cd.deployGhosttyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Ghostty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.NotEmpty(t, mainResult.BackupPath)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
assert.FileExists(t, mainResult.BackupPath)
|
||||
|
||||
backupContent, err := os.ReadFile(mainResult.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
|
||||
newContent, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(newContent), "# Old config")
|
||||
|
||||
colorResult := results[1]
|
||||
assert.Equal(t, "Ghostty Colors", colorResult.ConfigType)
|
||||
assert.True(t, colorResult.Deployed)
|
||||
assert.FileExists(t, colorResult.Path)
|
||||
})
|
||||
}
|
||||
|
||||
func getGhosttyPath() string {
|
||||
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
||||
}
|
||||
|
||||
func TestPolkitPathInjection(t *testing.T) {
|
||||
|
||||
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
||||
other content`
|
||||
|
||||
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
|
||||
|
||||
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
|
||||
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
|
||||
}
|
||||
|
||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||
cd := &ConfigDeployer{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newConfig string
|
||||
existingConfig string
|
||||
wantError bool
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "no existing monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================
|
||||
env = XDG_CURRENT_DESKTOP,niri`,
|
||||
existingConfig: `# Some other config
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{"MONITOR CONFIG", "ENVIRONMENT VARS"},
|
||||
},
|
||||
{
|
||||
name: "merge single monitor",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================`,
|
||||
existingConfig: `# My config
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"MONITOR CONFIG",
|
||||
"monitor = DP-1, 1920x1080@144, 0x0, 1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge multiple monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================`,
|
||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1
|
||||
monitor = eDP-1, 2560x1440@165, auto, 1.25`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"monitor = DP-1",
|
||||
"# monitor = HDMI-A-1", // Commented monitor preserved
|
||||
"monitor = eDP-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
wantNotContains: []string{
|
||||
"monitor = eDP-2", // Example monitor should be removed
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preserve commented monitors",
|
||||
newConfig: `# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
|
||||
# ==================`,
|
||||
existingConfig: `# monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
# monitor = HDMI-A-1, 1920x1080@60, 1920x0, 1`,
|
||||
wantError: false,
|
||||
wantContains: []string{
|
||||
"# monitor = DP-1",
|
||||
"# monitor = HDMI-A-1",
|
||||
"Monitors from existing configuration",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no monitor config section",
|
||||
newConfig: `# Some config without monitor section
|
||||
input {
|
||||
kb_layout = us
|
||||
}`,
|
||||
existingConfig: `monitor = DP-1, 1920x1080@144, 0x0, 1`,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
|
||||
|
||||
if tt.wantError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
assert.Contains(t, result, want, "merged config should contain: %s", want)
|
||||
}
|
||||
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
assert.NotContains(t, result, notWant, "merged config should NOT contain: %s", notWant)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandConfigDeployment(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-hyprland-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||
assert.True(t, result.Deployed)
|
||||
assert.Empty(t, result.BackupPath)
|
||||
assert.FileExists(t, result.Path)
|
||||
|
||||
content, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||
assert.Contains(t, string(content), "bind = $mod, T, exec, ghostty")
|
||||
assert.Contains(t, string(content), "exec-once = ")
|
||||
})
|
||||
|
||||
t.Run("deploy hyprland config with existing monitors", func(t *testing.T) {
|
||||
existingContent := `# My existing Hyprland config
|
||||
monitor = DP-1, 1920x1080@144, 0x0, 1
|
||||
monitor = HDMI-A-1, 3840x2160@60, 1920x0, 1.5
|
||||
|
||||
general {
|
||||
gaps_in = 10
|
||||
}
|
||||
`
|
||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||
assert.True(t, result.Deployed)
|
||||
assert.NotEmpty(t, result.BackupPath)
|
||||
assert.FileExists(t, result.Path)
|
||||
assert.FileExists(t, result.BackupPath)
|
||||
|
||||
backupContent, err := os.ReadFile(result.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
|
||||
newContent, err := os.ReadFile(result.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, kitty")
|
||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNiriConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, NiriConfig, "input {")
|
||||
assert.Contains(t, NiriConfig, "layout {")
|
||||
assert.Contains(t, NiriConfig, "binds {")
|
||||
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
||||
assert.Contains(t, NiriConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||
}
|
||||
|
||||
func TestHyprlandConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# ENVIRONMENT VARS")
|
||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
||||
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
||||
assert.Contains(t, HyprlandConfig, "{{TERMINAL_COMMAND}}")
|
||||
assert.Contains(t, HyprlandConfig, "exec-once = dms run")
|
||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec,")
|
||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
||||
assert.Contains(t, HyprlandConfig, "windowrulev2 = noborder, class:^(com\\.mitchellh\\.ghostty)$")
|
||||
}
|
||||
|
||||
func TestGhosttyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
|
||||
}
|
||||
|
||||
func TestGhosttyColorConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, GhosttyColorConfig, "background = #101418")
|
||||
assert.Contains(t, GhosttyColorConfig, "foreground = #e0e2e8")
|
||||
assert.Contains(t, GhosttyColorConfig, "cursor-color = #9dcbfb")
|
||||
assert.Contains(t, GhosttyColorConfig, "palette = 0=#101418")
|
||||
assert.Contains(t, GhosttyColorConfig, "palette = 15=#ffffff")
|
||||
}
|
||||
|
||||
func TestKittyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyConfig, "font_size 12.0")
|
||||
assert.Contains(t, KittyConfig, "window_padding_width 12")
|
||||
assert.Contains(t, KittyConfig, "background_opacity 1.0")
|
||||
assert.Contains(t, KittyConfig, "include dank-tabs.conf")
|
||||
assert.Contains(t, KittyConfig, "include dank-theme.conf")
|
||||
}
|
||||
|
||||
func TestKittyThemeConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
|
||||
assert.Contains(t, KittyThemeConfig, "background #101418")
|
||||
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
|
||||
assert.Contains(t, KittyThemeConfig, "color0 #101418")
|
||||
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
|
||||
}
|
||||
|
||||
func TestKittyTabsConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, KittyTabsConfig, "tab_bar_style powerline")
|
||||
assert.Contains(t, KittyTabsConfig, "tab_powerline_style slanted")
|
||||
assert.Contains(t, KittyTabsConfig, "active_tab_background #124a73")
|
||||
assert.Contains(t, KittyTabsConfig, "inactive_tab_background #101418")
|
||||
}
|
||||
|
||||
func TestAlacrittyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, AlacrittyConfig, "[general]")
|
||||
assert.Contains(t, AlacrittyConfig, "~/.config/alacritty/dank-theme.toml")
|
||||
assert.Contains(t, AlacrittyConfig, "[window]")
|
||||
assert.Contains(t, AlacrittyConfig, "decorations = \"None\"")
|
||||
assert.Contains(t, AlacrittyConfig, "padding = { x = 12, y = 12 }")
|
||||
assert.Contains(t, AlacrittyConfig, "[cursor]")
|
||||
assert.Contains(t, AlacrittyConfig, "[keyboard]")
|
||||
}
|
||||
|
||||
func TestAlacrittyThemeConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.primary]")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "background = '#101418'")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "foreground = '#e0e2e8'")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.cursor]")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "cursor = '#9dcbfb'")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.normal]")
|
||||
assert.Contains(t, AlacrittyThemeConfig, "[colors.bright]")
|
||||
}
|
||||
|
||||
func TestKittyConfigDeployment(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-kitty-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy kitty config to empty directory", func(t *testing.T) {
|
||||
results, err := cd.deployKittyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 3)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Kitty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
|
||||
content, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "include dank-theme.conf")
|
||||
|
||||
themeResult := results[1]
|
||||
assert.Equal(t, "Kitty Theme", themeResult.ConfigType)
|
||||
assert.True(t, themeResult.Deployed)
|
||||
assert.FileExists(t, themeResult.Path)
|
||||
|
||||
tabsResult := results[2]
|
||||
assert.Equal(t, "Kitty Tabs", tabsResult.ConfigType)
|
||||
assert.True(t, tabsResult.Deployed)
|
||||
assert.FileExists(t, tabsResult.Path)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlacrittyConfigDeployment(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "dankinstall-alacritty-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tempDir)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
cd := NewConfigDeployer(logChan)
|
||||
|
||||
t.Run("deploy alacritty config to empty directory", func(t *testing.T) {
|
||||
results, err := cd.deployAlacrittyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.Equal(t, "Alacritty", mainResult.ConfigType)
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.FileExists(t, mainResult.Path)
|
||||
|
||||
content, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), "~/.config/alacritty/dank-theme.toml")
|
||||
assert.Contains(t, string(content), "[window]")
|
||||
|
||||
themeResult := results[1]
|
||||
assert.Equal(t, "Alacritty Theme", themeResult.ConfigType)
|
||||
assert.True(t, themeResult.Deployed)
|
||||
assert.FileExists(t, themeResult.Path)
|
||||
|
||||
themeContent, err := os.ReadFile(themeResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(themeContent), "[colors.primary]")
|
||||
assert.Contains(t, string(themeContent), "background = '#101418'")
|
||||
})
|
||||
|
||||
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
||||
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
||||
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := cd.deployAlacrittyConfig()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 2)
|
||||
|
||||
mainResult := results[0]
|
||||
assert.True(t, mainResult.Deployed)
|
||||
assert.NotEmpty(t, mainResult.BackupPath)
|
||||
assert.FileExists(t, mainResult.BackupPath)
|
||||
|
||||
backupContent, err := os.ReadFile(mainResult.BackupPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingContent, string(backupContent))
|
||||
|
||||
newContent, err := os.ReadFile(mainResult.Path)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(newContent), "# Old alacritty config")
|
||||
assert.Contains(t, string(newContent), "decorations = \"None\"")
|
||||
})
|
||||
}
|
||||
46
internal/config/dms.go
Normal file
46
internal/config/dms.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
|
||||
func LocateDMSConfig() (string, error) {
|
||||
var searchPaths []string
|
||||
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
configHome = filepath.Join(homeDir, ".config")
|
||||
}
|
||||
}
|
||||
|
||||
if configHome != "" {
|
||||
searchPaths = append(searchPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||
}
|
||||
|
||||
searchPaths = append(searchPaths, "/usr/share/quickshell/dms")
|
||||
|
||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||
if configDirs == "" {
|
||||
configDirs = "/etc/xdg"
|
||||
}
|
||||
|
||||
for _, dir := range strings.Split(configDirs, ":") {
|
||||
if dir != "" {
|
||||
searchPaths = append(searchPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range searchPaths {
|
||||
shellPath := filepath.Join(path, "shell.qml")
|
||||
if info, err := os.Stat(shellPath); err == nil && !info.IsDir() {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not find DMS config (shell.qml) in any valid config path")
|
||||
}
|
||||
66
internal/config/embedded/.zshrc
Normal file
66
internal/config/embedded/.zshrc
Normal file
@@ -0,0 +1,66 @@
|
||||
fastfetch
|
||||
|
||||
# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
|
||||
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
|
||||
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
|
||||
fi
|
||||
|
||||
# Default exports
|
||||
export PATH="$PATH:/usr/bin/alacritty"
|
||||
export SDL_VIDEODRIVER=wayland
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
export PATH="$PATH:/opt/nvim-linux64/bin"
|
||||
fpath+=(${ZDOTDIR:-$HOME}/.zsh_functions)
|
||||
export EDITOR=nvim
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
HISTFILE=$HOME/.zhistory
|
||||
SAVEHIST=1000
|
||||
HISTSIZE=999
|
||||
|
||||
setopt share_history
|
||||
setopt hist_ignore_dups
|
||||
setopt hist_verify
|
||||
|
||||
# Tmux on by default
|
||||
if command -v tmux &> /dev/null && [ -n "$PS1" ] && [[ ! "$TERM" =~ screen ]] && [[ ! "$TERM" =~ tmux ]] && [ -z "$TMUX" ]; then
|
||||
exec tmux
|
||||
fi
|
||||
|
||||
|
||||
# Completion using arrow keys (based on history)
|
||||
bindkey '^[[A' history-search-backward
|
||||
zstyle ':completion:*' completer _expand _complete _ignored _correct _approximate
|
||||
zstyle ':completion:*' max-errors 5
|
||||
zstyle :compinstall filename '$HOME/.zshrc'
|
||||
autoload -Uz compinit
|
||||
compinit
|
||||
|
||||
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
|
||||
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
|
||||
|
||||
# Set aliases
|
||||
eval "$(zoxide init zsh)"
|
||||
|
||||
alias vim="nvim"
|
||||
alias cd="z"
|
||||
alias ls="eza --icons=always"
|
||||
alias sudo="doas"
|
||||
|
||||
# Enable and set vim keybindings
|
||||
set -o vi
|
||||
|
||||
bindkey -M viins 'jk' vi-cmd-mode
|
||||
bindkey -M vicmd '^R' history-incremental-search-backward # Ctrl+R for reverse search
|
||||
bindkey -M vicmd '/' history-incremental-search-backward # / for history search
|
||||
bindkey -M vicmd '?' history-incremental-search-forward # ? for forward search
|
||||
bindkey -M viins '^U' backward-kill-line # Ctrl+U to clear line in insert mode
|
||||
bindkey -M vicmd '^U' backward-kill-line # Ctrl+U to clear line in command mode
|
||||
bindkey -M vicmd '^A' beginning-of-line # Ctrl+A to go to the beginning of the line in command mode
|
||||
bindkey -M vicmd '^E' end-of-line # Ctrl+E to go to the end of the line in command mode
|
||||
|
||||
#source plugins
|
||||
source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh
|
||||
source ~/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
|
||||
source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme
|
||||
|
||||
31
internal/config/embedded/alacritty-theme.toml
Normal file
31
internal/config/embedded/alacritty-theme.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[colors.primary]
|
||||
background = '#101418'
|
||||
foreground = '#e0e2e8'
|
||||
|
||||
[colors.selection]
|
||||
text = '#e0e2e8'
|
||||
background = '#124a73'
|
||||
|
||||
[colors.cursor]
|
||||
text = '#101418'
|
||||
cursor = '#9dcbfb'
|
||||
|
||||
[colors.normal]
|
||||
black = '#101418'
|
||||
red = '#d75a59'
|
||||
green = '#8ed88c'
|
||||
yellow = '#e0d99d'
|
||||
blue = '#4087bc'
|
||||
magenta = '#839fbc'
|
||||
cyan = '#9dcbfb'
|
||||
white = '#abb2bf'
|
||||
|
||||
[colors.bright]
|
||||
black = '#5c6370'
|
||||
red = '#e57e7e'
|
||||
green = '#a2e5a0'
|
||||
yellow = '#efe9b3'
|
||||
blue = '#a7d9ff'
|
||||
magenta = '#3d8197'
|
||||
cyan = '#5c7ba3'
|
||||
white = '#ffffff'
|
||||
37
internal/config/embedded/alacritty.toml
Normal file
37
internal/config/embedded/alacritty.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
[general]
|
||||
import = [
|
||||
"~/.config/alacritty/dank-theme.toml"
|
||||
]
|
||||
|
||||
[window]
|
||||
decorations = "None"
|
||||
padding = { x = 12, y = 12 }
|
||||
opacity = 1.0
|
||||
|
||||
[scrolling]
|
||||
history = 3023
|
||||
|
||||
[cursor]
|
||||
style = { shape = "Block", blinking = "On" }
|
||||
blink_interval = 500
|
||||
unfocused_hollow = true
|
||||
|
||||
[mouse]
|
||||
hide_when_typing = true
|
||||
|
||||
[selection]
|
||||
save_to_clipboard = false
|
||||
|
||||
[bell]
|
||||
duration = 0
|
||||
|
||||
[keyboard]
|
||||
bindings = [
|
||||
{ key = "C", mods = "Control|Shift", action = "Copy" },
|
||||
{ key = "V", mods = "Control|Shift", action = "Paste" },
|
||||
{ key = "N", mods = "Control|Shift", action = "SpawnNewInstance" },
|
||||
{ key = "Equals", mods = "Control|Shift", action = "IncreaseFontSize" },
|
||||
{ key = "Minus", mods = "Control", action = "DecreaseFontSize" },
|
||||
{ key = "Key0", mods = "Control", action = "ResetFontSize" },
|
||||
{ key = "Enter", mods = "Shift", chars = "\n" },
|
||||
]
|
||||
21
internal/config/embedded/ghostty-colors.conf
Normal file
21
internal/config/embedded/ghostty-colors.conf
Normal file
@@ -0,0 +1,21 @@
|
||||
background = #101418
|
||||
foreground = #e0e2e8
|
||||
cursor-color = #9dcbfb
|
||||
selection-background = #124a73
|
||||
selection-foreground = #e0e2e8
|
||||
palette = 0=#101418
|
||||
palette = 1=#d75a59
|
||||
palette = 2=#8ed88c
|
||||
palette = 3=#e0d99d
|
||||
palette = 4=#4087bc
|
||||
palette = 5=#839fbc
|
||||
palette = 6=#9dcbfb
|
||||
palette = 7=#abb2bf
|
||||
palette = 8=#5c6370
|
||||
palette = 9=#e57e7e
|
||||
palette = 10=#a2e5a0
|
||||
palette = 11=#efe9b3
|
||||
palette = 12=#a7d9ff
|
||||
palette = 13=#3d8197
|
||||
palette = 14=#5c7ba3
|
||||
palette = 15=#ffffff
|
||||
51
internal/config/embedded/ghostty.conf
Normal file
51
internal/config/embedded/ghostty.conf
Normal file
@@ -0,0 +1,51 @@
|
||||
# Font Configuration
|
||||
font-size = 12
|
||||
|
||||
# Window Configuration
|
||||
window-decoration = false
|
||||
window-padding-x = 12
|
||||
window-padding-y = 12
|
||||
background-opacity = 1.0
|
||||
background-blur-radius = 32
|
||||
|
||||
# Cursor Configuration
|
||||
cursor-style = block
|
||||
cursor-style-blink = true
|
||||
|
||||
# Scrollback
|
||||
scrollback-limit = 3023
|
||||
|
||||
# Terminal features
|
||||
mouse-hide-while-typing = true
|
||||
copy-on-select = false
|
||||
confirm-close-surface = false
|
||||
|
||||
# Disable annoying copied to clipboard
|
||||
app-notifications = no-clipboard-copy,no-config-reload
|
||||
|
||||
# Key bindings for common actions
|
||||
#keybind = ctrl+c=copy_to_clipboard
|
||||
#keybind = ctrl+v=paste_from_clipboard
|
||||
keybind = ctrl+shift+n=new_window
|
||||
keybind = ctrl+t=new_tab
|
||||
keybind = ctrl+plus=increase_font_size:1
|
||||
keybind = ctrl+minus=decrease_font_size:1
|
||||
keybind = ctrl+zero=reset_font_size
|
||||
|
||||
# Material 3 UI elements
|
||||
unfocused-split-opacity = 0.7
|
||||
unfocused-split-fill = #44464f
|
||||
|
||||
# Tab configuration
|
||||
gtk-titlebar = false
|
||||
|
||||
# Shell integration
|
||||
shell-integration = detect
|
||||
shell-integration-features = cursor,sudo,title,no-cursor
|
||||
keybind = shift+enter=text:\n
|
||||
|
||||
# Rando stuff
|
||||
gtk-single-instance = true
|
||||
|
||||
# Dank color generation
|
||||
config-file = ./config-dankcolors
|
||||
290
internal/config/embedded/hyprland.conf
Normal file
290
internal/config/embedded/hyprland.conf
Normal file
@@ -0,0 +1,290 @@
|
||||
# Hyprland Configuration
|
||||
# https://wiki.hypr.land/Configuring/
|
||||
|
||||
# ==================
|
||||
# MONITOR CONFIG
|
||||
# ==================
|
||||
# monitor = eDP-2, 2560x1600@239.998993, 2560x0, 1, vrr, 1
|
||||
monitor = , preferred,auto,auto
|
||||
|
||||
# ==================
|
||||
# ENVIRONMENT VARS
|
||||
# ==================
|
||||
env = QT_QPA_PLATFORM,wayland
|
||||
env = ELECTRON_OZONE_PLATFORM_HINT,auto
|
||||
env = QT_QPA_PLATFORMTHEME,gtk3
|
||||
env = QT_QPA_PLATFORMTHEME_QT6,gtk3
|
||||
env = TERMINAL,{{TERMINAL_COMMAND}}
|
||||
|
||||
# ==================
|
||||
# STARTUP APPS
|
||||
# ==================
|
||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
||||
exec-once = dms run
|
||||
exec-once = {{POLKIT_AGENT_PATH}}
|
||||
|
||||
# ==================
|
||||
# INPUT CONFIG
|
||||
# ==================
|
||||
input {
|
||||
kb_layout = us
|
||||
numlock_by_default = true
|
||||
}
|
||||
|
||||
# ==================
|
||||
# GENERAL LAYOUT
|
||||
# ==================
|
||||
general {
|
||||
gaps_in = 5
|
||||
gaps_out = 5
|
||||
border_size = 0 # off in niri
|
||||
|
||||
col.active_border = rgba(707070ff)
|
||||
col.inactive_border = rgba(d0d0d0ff)
|
||||
|
||||
layout = dwindle
|
||||
}
|
||||
|
||||
# ==================
|
||||
# DECORATION
|
||||
# ==================
|
||||
decoration {
|
||||
rounding = 12
|
||||
|
||||
active_opacity = 1.0
|
||||
inactive_opacity = 0.9
|
||||
|
||||
shadow {
|
||||
enabled = true
|
||||
range = 30
|
||||
render_power = 5
|
||||
offset = 0 5
|
||||
color = rgba(00000070)
|
||||
}
|
||||
}
|
||||
|
||||
# ==================
|
||||
# ANIMATIONS
|
||||
# ==================
|
||||
animations {
|
||||
enabled = true
|
||||
|
||||
animation = windowsIn, 1, 3, default
|
||||
animation = windowsOut, 1, 3, default
|
||||
animation = workspaces, 1, 5, default
|
||||
animation = windowsMove, 1, 4, default
|
||||
animation = fade, 1, 3, default
|
||||
animation = border, 1, 3, default
|
||||
}
|
||||
|
||||
# ==================
|
||||
# LAYOUTS
|
||||
# ==================
|
||||
dwindle {
|
||||
preserve_split = true
|
||||
}
|
||||
|
||||
master {
|
||||
mfact = 0.5
|
||||
}
|
||||
|
||||
# ==================
|
||||
# MISC
|
||||
# ==================
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
disable_splash_rendering = true
|
||||
vrr = 1
|
||||
}
|
||||
|
||||
# ==================
|
||||
# WINDOW RULES
|
||||
# ==================
|
||||
windowrulev2 = tile, class:^(org\.wezfurlong\.wezterm)$
|
||||
|
||||
windowrulev2 = rounding 12, class:^(org\.gnome\.)
|
||||
windowrulev2 = noborder, class:^(org\.gnome\.)
|
||||
|
||||
windowrulev2 = tile, class:^(gnome-control-center)$
|
||||
windowrulev2 = tile, class:^(pavucontrol)$
|
||||
windowrulev2 = tile, class:^(nm-connection-editor)$
|
||||
|
||||
windowrulev2 = float, class:^(gnome-calculator)$
|
||||
windowrulev2 = float, class:^(galculator)$
|
||||
windowrulev2 = float, class:^(blueman-manager)$
|
||||
windowrulev2 = float, class:^(org\.gnome\.Nautilus)$
|
||||
windowrulev2 = float, class:^(steam)$
|
||||
windowrulev2 = float, class:^(xdg-desktop-portal)$
|
||||
|
||||
windowrulev2 = noborder, class:^(org\.wezfurlong\.wezterm)$
|
||||
windowrulev2 = noborder, class:^(Alacritty)$
|
||||
windowrulev2 = noborder, class:^(zen)$
|
||||
windowrulev2 = noborder, class:^(com\.mitchellh\.ghostty)$
|
||||
windowrulev2 = noborder, class:^(kitty)$
|
||||
|
||||
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
|
||||
windowrulev2 = float, class:^(zoom)$
|
||||
|
||||
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
|
||||
|
||||
layerrule = noanim, ^(quickshell)$
|
||||
|
||||
# ==================
|
||||
# KEYBINDINGS
|
||||
# ==================
|
||||
$mod = SUPER
|
||||
|
||||
# === Application Launchers ===
|
||||
bind = $mod, T, exec, {{TERMINAL_COMMAND}}
|
||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
||||
bind = $mod, M, exec, dms ipc call processlist toggle
|
||||
bind = $mod, comma, exec, dms ipc call settings toggle
|
||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
||||
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
|
||||
|
||||
# === Cheat sheet
|
||||
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
||||
|
||||
# === Security ===
|
||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
||||
bind = $mod SHIFT, E, exit
|
||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist toggle
|
||||
|
||||
# === Audio Controls ===
|
||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
||||
|
||||
# === Brightness Controls ===
|
||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||
|
||||
# === Window Management ===
|
||||
bind = $mod, Q, killactive
|
||||
bind = $mod, F, fullscreen, 1
|
||||
bind = $mod SHIFT, F, fullscreen, 0
|
||||
bind = $mod SHIFT, T, togglefloating
|
||||
bind = $mod, W, togglegroup
|
||||
|
||||
# === Focus Navigation ===
|
||||
bind = $mod, left, movefocus, l
|
||||
bind = $mod, down, movefocus, d
|
||||
bind = $mod, up, movefocus, u
|
||||
bind = $mod, right, movefocus, r
|
||||
bind = $mod, H, movefocus, l
|
||||
bind = $mod, J, movefocus, d
|
||||
bind = $mod, K, movefocus, u
|
||||
bind = $mod, L, movefocus, r
|
||||
|
||||
# === Window Movement ===
|
||||
bind = $mod SHIFT, left, movewindow, l
|
||||
bind = $mod SHIFT, down, movewindow, d
|
||||
bind = $mod SHIFT, up, movewindow, u
|
||||
bind = $mod SHIFT, right, movewindow, r
|
||||
bind = $mod SHIFT, H, movewindow, l
|
||||
bind = $mod SHIFT, J, movewindow, d
|
||||
bind = $mod SHIFT, K, movewindow, u
|
||||
bind = $mod SHIFT, L, movewindow, r
|
||||
|
||||
# === Column Navigation ===
|
||||
bind = $mod, Home, focuswindow, first
|
||||
bind = $mod, End, focuswindow, last
|
||||
|
||||
# === Monitor Navigation ===
|
||||
bind = $mod CTRL, left, focusmonitor, l
|
||||
bind = $mod CTRL, right, focusmonitor, r
|
||||
bind = $mod CTRL, H, focusmonitor, l
|
||||
bind = $mod CTRL, J, focusmonitor, d
|
||||
bind = $mod CTRL, K, focusmonitor, u
|
||||
bind = $mod CTRL, L, focusmonitor, r
|
||||
|
||||
# === Move to Monitor ===
|
||||
bind = $mod SHIFT CTRL, left, movewindow, mon:l
|
||||
bind = $mod SHIFT CTRL, down, movewindow, mon:d
|
||||
bind = $mod SHIFT CTRL, up, movewindow, mon:u
|
||||
bind = $mod SHIFT CTRL, right, movewindow, mon:r
|
||||
bind = $mod SHIFT CTRL, H, movewindow, mon:l
|
||||
bind = $mod SHIFT CTRL, J, movewindow, mon:d
|
||||
bind = $mod SHIFT CTRL, K, movewindow, mon:u
|
||||
bind = $mod SHIFT CTRL, L, movewindow, mon:r
|
||||
|
||||
# === Workspace Navigation ===
|
||||
bind = $mod, Page_Down, workspace, e+1
|
||||
bind = $mod, Page_Up, workspace, e-1
|
||||
bind = $mod, U, workspace, e+1
|
||||
bind = $mod, I, workspace, e-1
|
||||
bind = $mod CTRL, down, movetoworkspace, e+1
|
||||
bind = $mod CTRL, up, movetoworkspace, e-1
|
||||
bind = $mod CTRL, U, movetoworkspace, e+1
|
||||
bind = $mod CTRL, I, movetoworkspace, e-1
|
||||
|
||||
# === Move Workspaces ===
|
||||
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
|
||||
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
|
||||
bind = $mod SHIFT, U, movetoworkspace, e+1
|
||||
bind = $mod SHIFT, I, movetoworkspace, e-1
|
||||
|
||||
# === Mouse Wheel Navigation ===
|
||||
bind = $mod, mouse_down, workspace, e+1
|
||||
bind = $mod, mouse_up, workspace, e-1
|
||||
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
|
||||
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
|
||||
|
||||
# === Numbered Workspaces ===
|
||||
bind = $mod, 1, workspace, 1
|
||||
bind = $mod, 2, workspace, 2
|
||||
bind = $mod, 3, workspace, 3
|
||||
bind = $mod, 4, workspace, 4
|
||||
bind = $mod, 5, workspace, 5
|
||||
bind = $mod, 6, workspace, 6
|
||||
bind = $mod, 7, workspace, 7
|
||||
bind = $mod, 8, workspace, 8
|
||||
bind = $mod, 9, workspace, 9
|
||||
|
||||
# === Move to Numbered Workspaces ===
|
||||
bind = $mod SHIFT, 1, movetoworkspace, 1
|
||||
bind = $mod SHIFT, 2, movetoworkspace, 2
|
||||
bind = $mod SHIFT, 3, movetoworkspace, 3
|
||||
bind = $mod SHIFT, 4, movetoworkspace, 4
|
||||
bind = $mod SHIFT, 5, movetoworkspace, 5
|
||||
bind = $mod SHIFT, 6, movetoworkspace, 6
|
||||
bind = $mod SHIFT, 7, movetoworkspace, 7
|
||||
bind = $mod SHIFT, 8, movetoworkspace, 8
|
||||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
||||
|
||||
# === Column Management ===
|
||||
bind = $mod, bracketleft, layoutmsg, preselect l
|
||||
bind = $mod, bracketright, layoutmsg, preselect r
|
||||
|
||||
# === Sizing & Layout ===
|
||||
bind = $mod, R, layoutmsg, togglesplit
|
||||
bind = $mod CTRL, F, resizeactive, exact 100%
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindmd = $mod, mouse:272, Move window, movewindow
|
||||
bindmd = $mod, mouse:273, Resize window, resizewindow
|
||||
|
||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
|
||||
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
|
||||
|
||||
# === Manual Sizing ===
|
||||
binde = $mod, minus, resizeactive, -10% 0
|
||||
binde = $mod, equal, resizeactive, 10% 0
|
||||
binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
||||
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
||||
|
||||
# === Screenshots ===
|
||||
bind = , XF86Launch1, exec, grimblast copy area
|
||||
bind = CTRL, XF86Launch1, exec, grimblast copy screen
|
||||
bind = ALT, XF86Launch1, exec, grimblast copy active
|
||||
bind = , Print, exec, grimblast copy area
|
||||
bind = CTRL, Print, exec, grimblast copy screen
|
||||
bind = ALT, Print, exec, grimblast copy active
|
||||
|
||||
# === System Controls ===
|
||||
bind = $mod SHIFT, P, dpms, off
|
||||
24
internal/config/embedded/kitty-tabs.conf
Normal file
24
internal/config/embedded/kitty-tabs.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
tab_bar_edge top
|
||||
tab_bar_style powerline
|
||||
tab_powerline_style slanted
|
||||
tab_bar_align left
|
||||
tab_bar_min_tabs 2
|
||||
tab_bar_margin_width 0.0
|
||||
tab_bar_margin_height 2.5 1.5
|
||||
tab_bar_margin_color #101418
|
||||
|
||||
tab_bar_background #101418
|
||||
|
||||
active_tab_foreground #cfe5ff
|
||||
active_tab_background #124a73
|
||||
active_tab_font_style bold
|
||||
|
||||
inactive_tab_foreground #c2c7cf
|
||||
inactive_tab_background #101418
|
||||
inactive_tab_font_style normal
|
||||
|
||||
tab_activity_symbol " ● "
|
||||
tab_numbers_style 1
|
||||
|
||||
tab_title_template "{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title[:30]}{title[30:] and '…'} [{index}]"
|
||||
active_tab_title_template "{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{title[:30]}{title[30:] and '…'} [{index}]"
|
||||
24
internal/config/embedded/kitty-theme.conf
Normal file
24
internal/config/embedded/kitty-theme.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
cursor #e0e2e8
|
||||
cursor_text_color #c2c7cf
|
||||
|
||||
foreground #e0e2e8
|
||||
background #101418
|
||||
selection_foreground #243240
|
||||
selection_background #b9c8da
|
||||
url_color #9dcbfb
|
||||
color0 #101418
|
||||
color1 #d75a59
|
||||
color2 #8ed88c
|
||||
color3 #e0d99d
|
||||
color4 #4087bc
|
||||
color5 #839fbc
|
||||
color6 #9dcbfb
|
||||
color7 #abb2bf
|
||||
color8 #5c6370
|
||||
color9 #e57e7e
|
||||
color10 #a2e5a0
|
||||
color11 #efe9b3
|
||||
color12 #a7d9ff
|
||||
color13 #3d8197
|
||||
color14 #5c7ba3
|
||||
color15 #ffffff
|
||||
37
internal/config/embedded/kitty.conf
Normal file
37
internal/config/embedded/kitty.conf
Normal file
@@ -0,0 +1,37 @@
|
||||
# Font Configuration
|
||||
font_size 12.0
|
||||
|
||||
# Window Configuration
|
||||
window_padding_width 12
|
||||
background_opacity 1.0
|
||||
background_blur 32
|
||||
hide_window_decorations yes
|
||||
|
||||
# Cursor Configuration
|
||||
cursor_shape block
|
||||
cursor_blink_interval 1
|
||||
|
||||
# Scrollback
|
||||
scrollback_lines 3000
|
||||
|
||||
# Terminal features
|
||||
copy_on_select yes
|
||||
strip_trailing_spaces smart
|
||||
|
||||
# Key bindings for common actions
|
||||
map ctrl+shift+n new_window
|
||||
map ctrl+t new_tab
|
||||
map ctrl+plus change_font_size all +1.0
|
||||
map ctrl+minus change_font_size all -1.0
|
||||
map ctrl+0 change_font_size all 0
|
||||
|
||||
# Tab configuration
|
||||
tab_bar_style powerline
|
||||
tab_bar_align left
|
||||
|
||||
# Shell integration
|
||||
shell_integration enabled
|
||||
|
||||
# Dank color generation
|
||||
include dank-tabs.conf
|
||||
include dank-theme.conf
|
||||
418
internal/config/embedded/niri.kdl
Normal file
418
internal/config/embedded/niri.kdl
Normal file
@@ -0,0 +1,418 @@
|
||||
// This config is in the KDL format: https://kdl.dev
|
||||
// "/-" comments out the following node.
|
||||
// Check the wiki for a full description of the configuration:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Introduction
|
||||
config-notification {
|
||||
disable-failed
|
||||
}
|
||||
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
|
||||
// Input device configuration.
|
||||
// Find the full list of options on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Input
|
||||
input {
|
||||
keyboard {
|
||||
xkb {
|
||||
}
|
||||
numlock
|
||||
}
|
||||
touchpad {
|
||||
}
|
||||
mouse {
|
||||
}
|
||||
trackpoint {
|
||||
}
|
||||
}
|
||||
// You can configure outputs by their name, which you can find
|
||||
// by running `niri msg outputs` while inside a niri instance.
|
||||
// The built-in laptop monitor is usually called "eDP-1".
|
||||
// Find more information on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Outputs
|
||||
// Remember to uncomment the node by removing "/-"!
|
||||
/-output "eDP-2" {
|
||||
mode "2560x1600@239.998993"
|
||||
position x=2560 y=0
|
||||
variable-refresh-rate
|
||||
}
|
||||
// Settings that influence how windows are positioned and sized.
|
||||
// Find more information on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||
layout {
|
||||
// Set gaps around windows in logical pixels.
|
||||
gaps 5
|
||||
background-color "transparent"
|
||||
// When to center a column when changing focus, options are:
|
||||
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||
// or right edge of the screen.
|
||||
// - "always", the focused column will always be centered.
|
||||
// - "on-overflow", focusing a column will center it if it doesn't fit
|
||||
// together with the previously focused column.
|
||||
center-focused-column "never"
|
||||
// You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between.
|
||||
preset-column-widths {
|
||||
// Proportion sets the width as a fraction of the output width, taking gaps into account.
|
||||
// For example, you can perfectly fit four windows sized "proportion 0.25" on an output.
|
||||
// The default preset widths are 1/3, 1/2 and 2/3 of the output.
|
||||
proportion 0.33333
|
||||
proportion 0.5
|
||||
proportion 0.66667
|
||||
// Fixed sets the width in logical pixels exactly.
|
||||
// fixed 1920
|
||||
}
|
||||
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
|
||||
// preset-window-heights { }
|
||||
// You can change the default width of the new windows.
|
||||
default-column-width { proportion 0.5; }
|
||||
// If you leave the brackets empty, the windows themselves will decide their initial width.
|
||||
// default-column-width {}
|
||||
// By default focus ring and border are rendered as a solid background rectangle
|
||||
// behind windows. That is, they will show up through semitransparent windows.
|
||||
// This is because windows using client-side decorations can have an arbitrary shape.
|
||||
//
|
||||
// If you don't like that, you should uncomment `prefer-no-csd` below.
|
||||
// Niri will draw focus ring and border *around* windows that agree to omit their
|
||||
// client-side decorations.
|
||||
//
|
||||
// Alternatively, you can override it with a window rule called
|
||||
// `draw-border-with-background`.
|
||||
border {
|
||||
off
|
||||
width 4
|
||||
active-color "#707070" // Neutral gray
|
||||
inactive-color "#d0d0d0" // Light gray
|
||||
urgent-color "#cc4444" // Softer red
|
||||
}
|
||||
focus-ring {
|
||||
width 2
|
||||
active-color "#808080" // Medium gray
|
||||
inactive-color "#505050" // Dark gray
|
||||
}
|
||||
shadow {
|
||||
softness 30
|
||||
spread 5
|
||||
offset x=0 y=5
|
||||
color "#0007"
|
||||
}
|
||||
struts {
|
||||
}
|
||||
}
|
||||
layer-rule {
|
||||
match namespace="^quickshell$"
|
||||
place-within-backdrop true
|
||||
}
|
||||
overview {
|
||||
workspace-shadow {
|
||||
off
|
||||
}
|
||||
}
|
||||
// Add lines like this to spawn processes at startup.
|
||||
// Note that running niri as a session supports xdg-desktop-autostart,
|
||||
// which may be more convenient to use.
|
||||
// See the binds section below for more spawn examples.
|
||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
||||
spawn-at-startup "dms" "run"
|
||||
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
||||
environment {
|
||||
XDG_CURRENT_DESKTOP "niri"
|
||||
QT_QPA_PLATFORM "wayland"
|
||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||
QT_QPA_PLATFORMTHEME "gtk3"
|
||||
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||
TERMINAL "{{TERMINAL_COMMAND}}"
|
||||
}
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
prefer-no-csd
|
||||
screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
|
||||
animations {
|
||||
workspace-switch {
|
||||
spring damping-ratio=0.80 stiffness=523 epsilon=0.0001
|
||||
}
|
||||
window-open {
|
||||
duration-ms 150
|
||||
curve "ease-out-expo"
|
||||
}
|
||||
window-close {
|
||||
duration-ms 150
|
||||
curve "ease-out-quad"
|
||||
}
|
||||
horizontal-view-movement {
|
||||
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
|
||||
}
|
||||
window-movement {
|
||||
spring damping-ratio=0.75 stiffness=323 epsilon=0.0001
|
||||
}
|
||||
window-resize {
|
||||
spring damping-ratio=0.85 stiffness=423 epsilon=0.0001
|
||||
}
|
||||
config-notification-open-close {
|
||||
spring damping-ratio=0.65 stiffness=923 epsilon=0.001
|
||||
}
|
||||
screenshot-ui-open {
|
||||
duration-ms 200
|
||||
curve "ease-out-quad"
|
||||
}
|
||||
overview-open-close {
|
||||
spring damping-ratio=0.85 stiffness=800 epsilon=0.0001
|
||||
}
|
||||
}
|
||||
// Window rules let you adjust behavior for individual windows.
|
||||
// Find more information on the wiki:
|
||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Window-Rules
|
||||
// Work around WezTerm's initial configure bug
|
||||
// by setting an empty default-column-width.
|
||||
window-rule {
|
||||
// This regular expression is intentionally made as specific as possible,
|
||||
// since this is the default config, and we want no false positives.
|
||||
// You can get away with just app-id="wezterm" if you want.
|
||||
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||
default-column-width {}
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^org\.gnome\."#
|
||||
draw-border-with-background false
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^gnome-control-center$"#
|
||||
match app-id=r#"^pavucontrol$"#
|
||||
match app-id=r#"^nm-connection-editor$"#
|
||||
default-column-width { proportion 0.5; }
|
||||
open-floating false
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^gnome-calculator$"#
|
||||
match app-id=r#"^galculator$"#
|
||||
match app-id=r#"^blueman-manager$"#
|
||||
match app-id=r#"^org\.gnome\.Nautilus$"#
|
||||
match app-id=r#"^steam$"#
|
||||
match app-id=r#"^xdg-desktop-portal$"#
|
||||
open-floating true
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||
match app-id="Alacritty"
|
||||
match app-id="zen"
|
||||
match app-id="com.mitchellh.ghostty"
|
||||
match app-id="kitty"
|
||||
draw-border-with-background false
|
||||
}
|
||||
window-rule {
|
||||
match is-active=false
|
||||
opacity 0.9
|
||||
}
|
||||
window-rule {
|
||||
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||
match app-id="zoom"
|
||||
open-floating true
|
||||
}
|
||||
window-rule {
|
||||
geometry-corner-radius 12
|
||||
clip-to-geometry true
|
||||
}
|
||||
binds {
|
||||
// === System & Overview ===
|
||||
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
|
||||
Mod+Tab repeat=false { toggle-overview; }
|
||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||
|
||||
// === Application Launchers ===
|
||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "{{TERMINAL_COMMAND}}"; }
|
||||
Mod+Space hotkey-overlay-title="Application Launcher" {
|
||||
spawn "dms" "ipc" "call" "spotlight" "toggle";
|
||||
}
|
||||
Mod+V hotkey-overlay-title="Clipboard Manager" {
|
||||
spawn "dms" "ipc" "call" "clipboard" "toggle";
|
||||
}
|
||||
Mod+M hotkey-overlay-title="Task Manager" {
|
||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
||||
}
|
||||
Mod+Comma hotkey-overlay-title="Settings" {
|
||||
spawn "dms" "ipc" "call" "settings" "toggle";
|
||||
}
|
||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
||||
}
|
||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||
|
||||
// === Security ===
|
||||
Mod+Alt+L hotkey-overlay-title="Lock Screen" {
|
||||
spawn "dms" "ipc" "call" "lock" "lock";
|
||||
}
|
||||
Mod+Shift+E { quit; }
|
||||
Ctrl+Alt+Delete hotkey-overlay-title="Task Manager" {
|
||||
spawn "dms" "ipc" "call" "processlist" "toggle";
|
||||
}
|
||||
|
||||
// === Audio Controls ===
|
||||
XF86AudioRaiseVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "increment" "3";
|
||||
}
|
||||
XF86AudioLowerVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "decrement" "3";
|
||||
}
|
||||
XF86AudioMute allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "mute";
|
||||
}
|
||||
XF86AudioMicMute allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||
}
|
||||
|
||||
// === Brightness Controls ===
|
||||
XF86MonBrightnessUp allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "brightness" "increment" "5" "";
|
||||
}
|
||||
XF86MonBrightnessDown allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "brightness" "decrement" "5" "";
|
||||
}
|
||||
|
||||
// === Window Management ===
|
||||
Mod+Q repeat=false { close-window; }
|
||||
Mod+F { maximize-column; }
|
||||
Mod+Shift+F { fullscreen-window; }
|
||||
Mod+Shift+T { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
Mod+W { toggle-column-tabbed-display; }
|
||||
|
||||
// === Focus Navigation ===
|
||||
Mod+Left { focus-column-left; }
|
||||
Mod+Down { focus-window-down; }
|
||||
Mod+Up { focus-window-up; }
|
||||
Mod+Right { focus-column-right; }
|
||||
Mod+H { focus-column-left; }
|
||||
Mod+J { focus-window-down; }
|
||||
Mod+K { focus-window-up; }
|
||||
Mod+L { focus-column-right; }
|
||||
|
||||
// === Window Movement ===
|
||||
Mod+Shift+Left { move-column-left; }
|
||||
Mod+Shift+Down { move-window-down; }
|
||||
Mod+Shift+Up { move-window-up; }
|
||||
Mod+Shift+Right { move-column-right; }
|
||||
Mod+Shift+H { move-column-left; }
|
||||
Mod+Shift+J { move-window-down; }
|
||||
Mod+Shift+K { move-window-up; }
|
||||
Mod+Shift+L { move-column-right; }
|
||||
|
||||
// === Column Navigation ===
|
||||
Mod+Home { focus-column-first; }
|
||||
Mod+End { focus-column-last; }
|
||||
Mod+Ctrl+Home { move-column-to-first; }
|
||||
Mod+Ctrl+End { move-column-to-last; }
|
||||
|
||||
// === Monitor Navigation ===
|
||||
Mod+Ctrl+Left { focus-monitor-left; }
|
||||
//Mod+Ctrl+Down { focus-monitor-down; }
|
||||
//Mod+Ctrl+Up { focus-monitor-up; }
|
||||
Mod+Ctrl+Right { focus-monitor-right; }
|
||||
Mod+Ctrl+H { focus-monitor-left; }
|
||||
Mod+Ctrl+J { focus-monitor-down; }
|
||||
Mod+Ctrl+K { focus-monitor-up; }
|
||||
Mod+Ctrl+L { focus-monitor-right; }
|
||||
|
||||
// === Move to Monitor ===
|
||||
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||
Mod+Shift+Ctrl+H { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+J { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+K { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+L { move-column-to-monitor-right; }
|
||||
|
||||
// === Workspace Navigation ===
|
||||
Mod+Page_Down { focus-workspace-down; }
|
||||
Mod+Page_Up { focus-workspace-up; }
|
||||
Mod+U { focus-workspace-down; }
|
||||
Mod+I { focus-workspace-up; }
|
||||
Mod+Ctrl+Down { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+Up { move-column-to-workspace-up; }
|
||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||
|
||||
// === Move Workspaces ===
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
Mod+Shift+U { move-workspace-down; }
|
||||
Mod+Shift+I { move-workspace-up; }
|
||||
|
||||
// === Mouse Wheel Navigation ===
|
||||
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
|
||||
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
|
||||
Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
|
||||
|
||||
Mod+WheelScrollRight { focus-column-right; }
|
||||
Mod+WheelScrollLeft { focus-column-left; }
|
||||
Mod+Ctrl+WheelScrollRight { move-column-right; }
|
||||
Mod+Ctrl+WheelScrollLeft { move-column-left; }
|
||||
|
||||
Mod+Shift+WheelScrollDown { focus-column-right; }
|
||||
Mod+Shift+WheelScrollUp { focus-column-left; }
|
||||
Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
|
||||
Mod+Ctrl+Shift+WheelScrollUp { move-column-left; }
|
||||
|
||||
// === Numbered Workspaces ===
|
||||
Mod+1 { focus-workspace 1; }
|
||||
Mod+2 { focus-workspace 2; }
|
||||
Mod+3 { focus-workspace 3; }
|
||||
Mod+4 { focus-workspace 4; }
|
||||
Mod+5 { focus-workspace 5; }
|
||||
Mod+6 { focus-workspace 6; }
|
||||
Mod+7 { focus-workspace 7; }
|
||||
Mod+8 { focus-workspace 8; }
|
||||
Mod+9 { focus-workspace 9; }
|
||||
|
||||
// === Move to Numbered Workspaces ===
|
||||
Mod+Shift+1 { move-column-to-workspace 1; }
|
||||
Mod+Shift+2 { move-column-to-workspace 2; }
|
||||
Mod+Shift+3 { move-column-to-workspace 3; }
|
||||
Mod+Shift+4 { move-column-to-workspace 4; }
|
||||
Mod+Shift+5 { move-column-to-workspace 5; }
|
||||
Mod+Shift+6 { move-column-to-workspace 6; }
|
||||
Mod+Shift+7 { move-column-to-workspace 7; }
|
||||
Mod+Shift+8 { move-column-to-workspace 8; }
|
||||
Mod+Shift+9 { move-column-to-workspace 9; }
|
||||
|
||||
// === Column Management ===
|
||||
Mod+BracketLeft { consume-or-expel-window-left; }
|
||||
Mod+BracketRight { consume-or-expel-window-right; }
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
// === Sizing & Layout ===
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { switch-preset-window-height; }
|
||||
Mod+Ctrl+R { reset-window-height; }
|
||||
Mod+Ctrl+F { expand-column-to-available-width; }
|
||||
Mod+C { center-column; }
|
||||
Mod+Ctrl+C { center-visible-columns; }
|
||||
|
||||
// === Manual Sizing ===
|
||||
Mod+Minus { set-column-width "-10%"; }
|
||||
Mod+Equal { set-column-width "+10%"; }
|
||||
Mod+Shift+Minus { set-window-height "-10%"; }
|
||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||
|
||||
// === Screenshots ===
|
||||
XF86Launch1 { screenshot; }
|
||||
Ctrl+XF86Launch1 { screenshot-screen; }
|
||||
Alt+XF86Launch1 { screenshot-window; }
|
||||
Print { screenshot; }
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
// === System Controls ===
|
||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||
Mod+Shift+P { power-off-monitors; }
|
||||
}
|
||||
debug {
|
||||
honor-xdg-activation-with-invalid-serial
|
||||
}
|
||||
6
internal/config/hyprland.go
Normal file
6
internal/config/hyprland.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed embedded/hyprland.conf
|
||||
var HyprlandConfig string
|
||||
6
internal/config/niri.go
Normal file
6
internal/config/niri.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed embedded/niri.kdl
|
||||
var NiriConfig string
|
||||
6
internal/config/shells.go
Normal file
6
internal/config/shells.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed embedded/.zshrc
|
||||
var ZshrcConfig string
|
||||
24
internal/config/terminals.go
Normal file
24
internal/config/terminals.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed embedded/ghostty.conf
|
||||
var GhosttyConfig string
|
||||
|
||||
//go:embed embedded/ghostty-colors.conf
|
||||
var GhosttyColorConfig string
|
||||
|
||||
//go:embed embedded/kitty.conf
|
||||
var KittyConfig string
|
||||
|
||||
//go:embed embedded/kitty-theme.conf
|
||||
var KittyThemeConfig string
|
||||
|
||||
//go:embed embedded/kitty-tabs.conf
|
||||
var KittyTabsConfig string
|
||||
|
||||
//go:embed embedded/alacritty.toml
|
||||
var AlacrittyConfig string
|
||||
|
||||
//go:embed embedded/alacritty-theme.toml
|
||||
var AlacrittyThemeConfig string
|
||||
453
internal/dank16/dank16.go
Normal file
453
internal/dank16/dank16.go
Normal file
@@ -0,0 +1,453 @@
|
||||
package dank16
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
type RGB struct {
|
||||
R, G, B float64
|
||||
}
|
||||
|
||||
type HSV struct {
|
||||
H, S, V float64
|
||||
}
|
||||
|
||||
func HexToRGB(hex string) RGB {
|
||||
if hex[0] == '#' {
|
||||
hex = hex[1:]
|
||||
}
|
||||
var r, g, b uint8
|
||||
fmt.Sscanf(hex, "%02x%02x%02x", &r, &g, &b)
|
||||
return RGB{
|
||||
R: float64(r) / 255.0,
|
||||
G: float64(g) / 255.0,
|
||||
B: float64(b) / 255.0,
|
||||
}
|
||||
}
|
||||
|
||||
func RGBToHex(rgb RGB) string {
|
||||
r := math.Max(0, math.Min(1, rgb.R))
|
||||
g := math.Max(0, math.Min(1, rgb.G))
|
||||
b := math.Max(0, math.Min(1, rgb.B))
|
||||
return fmt.Sprintf("#%02x%02x%02x", int(r*255), int(g*255), int(b*255))
|
||||
}
|
||||
|
||||
func RGBToHSV(rgb RGB) HSV {
|
||||
max := math.Max(math.Max(rgb.R, rgb.G), rgb.B)
|
||||
min := math.Min(math.Min(rgb.R, rgb.G), rgb.B)
|
||||
delta := max - min
|
||||
|
||||
var h float64
|
||||
if delta == 0 {
|
||||
h = 0
|
||||
} else if max == rgb.R {
|
||||
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
|
||||
} else if max == rgb.G {
|
||||
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
|
||||
} else {
|
||||
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
|
||||
}
|
||||
|
||||
if h < 0 {
|
||||
h += 1.0
|
||||
}
|
||||
|
||||
var s float64
|
||||
if max == 0 {
|
||||
s = 0
|
||||
} else {
|
||||
s = delta / max
|
||||
}
|
||||
|
||||
return HSV{H: h, S: s, V: max}
|
||||
}
|
||||
|
||||
func HSVToRGB(hsv HSV) RGB {
|
||||
h := hsv.H * 6.0
|
||||
c := hsv.V * hsv.S
|
||||
x := c * (1.0 - math.Abs(math.Mod(h, 2.0)-1.0))
|
||||
m := hsv.V - c
|
||||
|
||||
var r, g, b float64
|
||||
switch int(h) {
|
||||
case 0:
|
||||
r, g, b = c, x, 0
|
||||
case 1:
|
||||
r, g, b = x, c, 0
|
||||
case 2:
|
||||
r, g, b = 0, c, x
|
||||
case 3:
|
||||
r, g, b = 0, x, c
|
||||
case 4:
|
||||
r, g, b = x, 0, c
|
||||
case 5:
|
||||
r, g, b = c, 0, x
|
||||
default:
|
||||
r, g, b = c, 0, x
|
||||
}
|
||||
|
||||
return RGB{R: r + m, G: g + m, B: b + m}
|
||||
}
|
||||
|
||||
func sRGBToLinear(c float64) float64 {
|
||||
if c <= 0.04045 {
|
||||
return c / 12.92
|
||||
}
|
||||
return math.Pow((c+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
func Luminance(hex string) float64 {
|
||||
rgb := HexToRGB(hex)
|
||||
return 0.2126*sRGBToLinear(rgb.R) + 0.7152*sRGBToLinear(rgb.G) + 0.0722*sRGBToLinear(rgb.B)
|
||||
}
|
||||
|
||||
func ContrastRatio(hexFg, hexBg string) float64 {
|
||||
lumFg := Luminance(hexFg)
|
||||
lumBg := Luminance(hexBg)
|
||||
lighter := math.Max(lumFg, lumBg)
|
||||
darker := math.Min(lumFg, lumBg)
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
}
|
||||
|
||||
func getLstar(hex string) float64 {
|
||||
rgb := HexToRGB(hex)
|
||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||
L, _, _ := col.Lab()
|
||||
return L * 100.0 // go-colorful uses 0-1, we need 0-100 for DPS
|
||||
}
|
||||
|
||||
// Lab to hex, clamping if needed
|
||||
func labToHex(L, a, b float64) string {
|
||||
c := colorful.Lab(L/100.0, a, b) // back to 0-1 for go-colorful
|
||||
r, g, b2 := c.Clamped().RGB255()
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||
}
|
||||
|
||||
// Adjust brightness while keeping the same hue
|
||||
func retoneToL(hex string, Ltarget float64) string {
|
||||
rgb := HexToRGB(hex)
|
||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
||||
L, a, b := col.Lab()
|
||||
L100 := L * 100.0
|
||||
|
||||
scale := 1.0
|
||||
if L100 != 0 {
|
||||
scale = Ltarget / L100
|
||||
}
|
||||
|
||||
a2, b2 := a*scale, b*scale
|
||||
|
||||
// Don't let it get too saturated
|
||||
maxChroma := 0.4
|
||||
if math.Hypot(a2, b2) > maxChroma {
|
||||
k := maxChroma / math.Hypot(a2, b2)
|
||||
a2 *= k
|
||||
b2 *= k
|
||||
}
|
||||
|
||||
return labToHex(Ltarget, a2, b2)
|
||||
}
|
||||
|
||||
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||
Lf := getLstar(hexFg)
|
||||
Lb := getLstar(hexBg)
|
||||
|
||||
phi := 1.618
|
||||
inv := 0.618
|
||||
lc := math.Pow(math.Abs(math.Pow(Lb, phi)-math.Pow(Lf, phi)), inv)*1.414 - 40
|
||||
|
||||
if negativePolarity {
|
||||
lc += 5
|
||||
}
|
||||
|
||||
return lc
|
||||
}
|
||||
|
||||
func DeltaPhiStarContrast(hexFg, hexBg string, isLightMode bool) float64 {
|
||||
negativePolarity := !isLightMode
|
||||
return DeltaPhiStar(hexFg, hexBg, negativePolarity)
|
||||
}
|
||||
|
||||
func EnsureContrast(hexColor, hexBg string, minRatio float64, isLightMode bool) string {
|
||||
currentRatio := ContrastRatio(hexColor, hexBg)
|
||||
if currentRatio >= minRatio {
|
||||
return hexColor
|
||||
}
|
||||
|
||||
rgb := HexToRGB(hexColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
for step := 1; step < 30; step++ {
|
||||
delta := float64(step) * 0.02
|
||||
|
||||
if isLightMode {
|
||||
newV := math.Max(0, hsv.V-delta)
|
||||
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||
return candidate
|
||||
}
|
||||
|
||||
newV = math.Min(1, hsv.V+delta)
|
||||
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||
return candidate
|
||||
}
|
||||
} else {
|
||||
newV := math.Min(1, hsv.V+delta)
|
||||
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||
return candidate
|
||||
}
|
||||
|
||||
newV = math.Max(0, hsv.V-delta)
|
||||
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if ContrastRatio(candidate, hexBg) >= minRatio {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hexColor
|
||||
}
|
||||
|
||||
func EnsureContrastDPS(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||
currentLc := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||
if currentLc >= minLc {
|
||||
return hexColor
|
||||
}
|
||||
|
||||
rgb := HexToRGB(hexColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
for step := 1; step < 50; step++ {
|
||||
delta := float64(step) * 0.015
|
||||
|
||||
if isLightMode {
|
||||
newV := math.Max(0, hsv.V-delta)
|
||||
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||
return candidate
|
||||
}
|
||||
|
||||
newV = math.Min(1, hsv.V+delta)
|
||||
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||
return candidate
|
||||
}
|
||||
} else {
|
||||
newV := math.Min(1, hsv.V+delta)
|
||||
candidate := RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||
return candidate
|
||||
}
|
||||
|
||||
newV = math.Max(0, hsv.V-delta)
|
||||
candidate = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: hsv.S, V: newV}))
|
||||
if DeltaPhiStarContrast(candidate, hexBg, isLightMode) >= minLc {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hexColor
|
||||
}
|
||||
|
||||
// Nudge L* until contrast is good enough. Keeps hue intact unlike HSV fiddling.
|
||||
func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||
if current >= minLc {
|
||||
return hexColor
|
||||
}
|
||||
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
Lf, af, bf := cf.Lab()
|
||||
|
||||
dir := 1.0
|
||||
if isLightMode {
|
||||
dir = -1.0 // light mode = darker text
|
||||
}
|
||||
|
||||
step := 0.5
|
||||
for i := 0; i < 120; i++ {
|
||||
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
||||
cand := labToHex(Lf, af, bf)
|
||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
|
||||
return hexColor
|
||||
}
|
||||
|
||||
type PaletteOptions struct {
|
||||
IsLight bool
|
||||
Background string
|
||||
UseDPS bool
|
||||
}
|
||||
|
||||
func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||
if opts.UseDPS {
|
||||
return EnsureContrastDPSLstar(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||
}
|
||||
|
||||
func DeriveContainer(primary string, isLight bool) string {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
if isLight {
|
||||
containerV := math.Min(hsv.V*1.77, 1.0)
|
||||
containerS := hsv.S * 0.32
|
||||
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||
}
|
||||
containerV := hsv.V * 0.463
|
||||
containerS := math.Min(hsv.S*1.834, 1.0)
|
||||
return RGBToHex(HSVToRGB(HSV{H: hsv.H, S: containerS, V: containerV}))
|
||||
}
|
||||
|
||||
func GeneratePalette(primaryColor string, opts PaletteOptions) []string {
|
||||
baseColor := DeriveContainer(primaryColor, opts.IsLight)
|
||||
|
||||
rgb := HexToRGB(baseColor)
|
||||
hsv := RGBToHSV(rgb)
|
||||
|
||||
palette := make([]string, 0, 16)
|
||||
|
||||
var normalTextTarget, secondaryTarget float64
|
||||
if opts.UseDPS {
|
||||
normalTextTarget = 40.0
|
||||
secondaryTarget = 35.0
|
||||
} else {
|
||||
normalTextTarget = 4.5
|
||||
secondaryTarget = 3.0
|
||||
}
|
||||
|
||||
var bgColor string
|
||||
if opts.Background != "" {
|
||||
bgColor = opts.Background
|
||||
} else if opts.IsLight {
|
||||
bgColor = "#f8f8f8"
|
||||
} else {
|
||||
bgColor = "#1a1a1a"
|
||||
}
|
||||
palette = append(palette, bgColor)
|
||||
|
||||
hueShift := (hsv.H - 0.6) * 0.12
|
||||
satBoost := 1.15
|
||||
|
||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
||||
var redColor string
|
||||
if opts.IsLight {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
||||
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
||||
palette = append(palette, ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
||||
var greenColor string
|
||||
if opts.IsLight {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
||||
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
||||
palette = append(palette, ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
||||
var yellowColor string
|
||||
if opts.IsLight {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
||||
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
||||
palette = append(palette, ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
var blueColor string
|
||||
if opts.IsLight {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
||||
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
||||
palette = append(palette, ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
magH := hsv.H - 0.03
|
||||
if magH < 0 {
|
||||
magH += 1.0
|
||||
}
|
||||
var magColor string
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
if opts.IsLight {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
||||
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
} else {
|
||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
||||
palette = append(palette, ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
||||
}
|
||||
|
||||
cyanH := hsv.H + 0.08
|
||||
if cyanH > 1.0 {
|
||||
cyanH -= 1.0
|
||||
}
|
||||
palette = append(palette, ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
||||
|
||||
if opts.IsLight {
|
||||
palette = append(palette, "#1a1a1a")
|
||||
palette = append(palette, "#2e2e2e")
|
||||
} else {
|
||||
palette = append(palette, "#abb2bf")
|
||||
palette = append(palette, "#5c6370")
|
||||
}
|
||||
|
||||
if opts.IsLight {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
||||
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
||||
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
||||
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
hr := HexToRGB(primaryColor)
|
||||
hh := RGBToHSV(hr)
|
||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
||||
palette = append(palette, ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
||||
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
} else {
|
||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
||||
palette = append(palette, ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
||||
palette = append(palette, ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
||||
palette = append(palette, ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
||||
// Make it way brighter for type names in dark mode
|
||||
brightBlue := retoneToL(primaryColor, 85.0)
|
||||
palette = append(palette, brightBlue)
|
||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
||||
palette = append(palette, ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
||||
brightCyanH := hsv.H + 0.02
|
||||
if brightCyanH > 1.0 {
|
||||
brightCyanH -= 1.0
|
||||
}
|
||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
||||
palette = append(palette, ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
||||
}
|
||||
|
||||
if opts.IsLight {
|
||||
palette = append(palette, "#1a1a1a")
|
||||
} else {
|
||||
palette = append(palette, "#ffffff")
|
||||
}
|
||||
|
||||
return palette
|
||||
}
|
||||
727
internal/dank16/dank16_test.go
Normal file
727
internal/dank16/dank16_test.go
Normal file
@@ -0,0 +1,727 @@
|
||||
package dank16
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHexToRGB(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected RGB
|
||||
}{
|
||||
{
|
||||
name: "black with hash",
|
||||
input: "#000000",
|
||||
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||
},
|
||||
{
|
||||
name: "white with hash",
|
||||
input: "#ffffff",
|
||||
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||
},
|
||||
{
|
||||
name: "red without hash",
|
||||
input: "ff0000",
|
||||
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||
},
|
||||
{
|
||||
name: "purple",
|
||||
input: "#625690",
|
||||
expected: RGB{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
|
||||
},
|
||||
{
|
||||
name: "mid gray",
|
||||
input: "#808080",
|
||||
expected: RGB{R: 0.5019607843137255, G: 0.5019607843137255, B: 0.5019607843137255},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := HexToRGB(tt.input)
|
||||
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
|
||||
t.Errorf("HexToRGB(%s) = %v, expected %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRGBToHex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input RGB
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "black",
|
||||
input: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||
expected: "#000000",
|
||||
},
|
||||
{
|
||||
name: "white",
|
||||
input: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||
expected: "#ffffff",
|
||||
},
|
||||
{
|
||||
name: "red",
|
||||
input: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||
expected: "#ff0000",
|
||||
},
|
||||
{
|
||||
name: "clamping above 1.0",
|
||||
input: RGB{R: 1.5, G: 0.5, B: 0.5},
|
||||
expected: "#ff7f7f",
|
||||
},
|
||||
{
|
||||
name: "clamping below 0.0",
|
||||
input: RGB{R: -0.5, G: 0.5, B: 0.5},
|
||||
expected: "#007f7f",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := RGBToHex(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("RGBToHex(%v) = %s, expected %s", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRGBToHSV(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input RGB
|
||||
expected HSV
|
||||
}{
|
||||
{
|
||||
name: "black",
|
||||
input: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||
expected: HSV{H: 0.0, S: 0.0, V: 0.0},
|
||||
},
|
||||
{
|
||||
name: "white",
|
||||
input: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||
expected: HSV{H: 0.0, S: 0.0, V: 1.0},
|
||||
},
|
||||
{
|
||||
name: "red",
|
||||
input: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||
expected: HSV{H: 0.0, S: 1.0, V: 1.0},
|
||||
},
|
||||
{
|
||||
name: "green",
|
||||
input: RGB{R: 0.0, G: 1.0, B: 0.0},
|
||||
expected: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
|
||||
},
|
||||
{
|
||||
name: "blue",
|
||||
input: RGB{R: 0.0, G: 0.0, B: 1.0},
|
||||
expected: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := RGBToHSV(tt.input)
|
||||
if !floatEqual(result.H, tt.expected.H) || !floatEqual(result.S, tt.expected.S) || !floatEqual(result.V, tt.expected.V) {
|
||||
t.Errorf("RGBToHSV(%v) = %v, expected %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHSVToRGB(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input HSV
|
||||
expected RGB
|
||||
}{
|
||||
{
|
||||
name: "black",
|
||||
input: HSV{H: 0.0, S: 0.0, V: 0.0},
|
||||
expected: RGB{R: 0.0, G: 0.0, B: 0.0},
|
||||
},
|
||||
{
|
||||
name: "white",
|
||||
input: HSV{H: 0.0, S: 0.0, V: 1.0},
|
||||
expected: RGB{R: 1.0, G: 1.0, B: 1.0},
|
||||
},
|
||||
{
|
||||
name: "red",
|
||||
input: HSV{H: 0.0, S: 1.0, V: 1.0},
|
||||
expected: RGB{R: 1.0, G: 0.0, B: 0.0},
|
||||
},
|
||||
{
|
||||
name: "green",
|
||||
input: HSV{H: 0.3333333333333333, S: 1.0, V: 1.0},
|
||||
expected: RGB{R: 0.0, G: 1.0, B: 0.0},
|
||||
},
|
||||
{
|
||||
name: "blue",
|
||||
input: HSV{H: 0.6666666666666666, S: 1.0, V: 1.0},
|
||||
expected: RGB{R: 0.0, G: 0.0, B: 1.0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := HSVToRGB(tt.input)
|
||||
if !floatEqual(result.R, tt.expected.R) || !floatEqual(result.G, tt.expected.G) || !floatEqual(result.B, tt.expected.B) {
|
||||
t.Errorf("HSVToRGB(%v) = %v, expected %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLuminance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected float64
|
||||
}{
|
||||
{
|
||||
name: "black",
|
||||
input: "#000000",
|
||||
expected: 0.0,
|
||||
},
|
||||
{
|
||||
name: "white",
|
||||
input: "#ffffff",
|
||||
expected: 1.0,
|
||||
},
|
||||
{
|
||||
name: "red",
|
||||
input: "#ff0000",
|
||||
expected: 0.2126,
|
||||
},
|
||||
{
|
||||
name: "green",
|
||||
input: "#00ff00",
|
||||
expected: 0.7152,
|
||||
},
|
||||
{
|
||||
name: "blue",
|
||||
input: "#0000ff",
|
||||
expected: 0.0722,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Luminance(tt.input)
|
||||
if !floatEqual(result, tt.expected) {
|
||||
t.Errorf("Luminance(%s) = %f, expected %f", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrastRatio(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fg string
|
||||
bg string
|
||||
expected float64
|
||||
}{
|
||||
{
|
||||
name: "black on white",
|
||||
fg: "#000000",
|
||||
bg: "#ffffff",
|
||||
expected: 21.0,
|
||||
},
|
||||
{
|
||||
name: "white on black",
|
||||
fg: "#ffffff",
|
||||
bg: "#000000",
|
||||
expected: 21.0,
|
||||
},
|
||||
{
|
||||
name: "same color",
|
||||
fg: "#808080",
|
||||
bg: "#808080",
|
||||
expected: 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ContrastRatio(tt.fg, tt.bg)
|
||||
if !floatEqual(result, tt.expected) {
|
||||
t.Errorf("ContrastRatio(%s, %s) = %f, expected %f", tt.fg, tt.bg, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureContrast(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
color string
|
||||
bg string
|
||||
minRatio float64
|
||||
isLightMode bool
|
||||
}{
|
||||
{
|
||||
name: "already sufficient contrast dark mode",
|
||||
color: "#ffffff",
|
||||
bg: "#000000",
|
||||
minRatio: 4.5,
|
||||
isLightMode: false,
|
||||
},
|
||||
{
|
||||
name: "already sufficient contrast light mode",
|
||||
color: "#000000",
|
||||
bg: "#ffffff",
|
||||
minRatio: 4.5,
|
||||
isLightMode: true,
|
||||
},
|
||||
{
|
||||
name: "needs adjustment dark mode",
|
||||
color: "#404040",
|
||||
bg: "#1a1a1a",
|
||||
minRatio: 4.5,
|
||||
isLightMode: false,
|
||||
},
|
||||
{
|
||||
name: "needs adjustment light mode",
|
||||
color: "#c0c0c0",
|
||||
bg: "#f8f8f8",
|
||||
minRatio: 4.5,
|
||||
isLightMode: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := EnsureContrast(tt.color, tt.bg, tt.minRatio, tt.isLightMode)
|
||||
actualRatio := ContrastRatio(result, tt.bg)
|
||||
if actualRatio < tt.minRatio {
|
||||
t.Errorf("EnsureContrast(%s, %s, %f, %t) = %s with ratio %f, expected ratio >= %f",
|
||||
tt.color, tt.bg, tt.minRatio, tt.isLightMode, result, actualRatio, tt.minRatio)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePalette(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
base string
|
||||
opts PaletteOptions
|
||||
}{
|
||||
{
|
||||
name: "dark theme default",
|
||||
base: "#625690",
|
||||
opts: PaletteOptions{IsLight: false},
|
||||
},
|
||||
{
|
||||
name: "light theme default",
|
||||
base: "#625690",
|
||||
opts: PaletteOptions{IsLight: true},
|
||||
},
|
||||
{
|
||||
name: "light theme with custom background",
|
||||
base: "#625690",
|
||||
opts: PaletteOptions{
|
||||
IsLight: true,
|
||||
Background: "#fafafa",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dark theme with custom background",
|
||||
base: "#625690",
|
||||
opts: PaletteOptions{
|
||||
IsLight: false,
|
||||
Background: "#0a0a0a",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GeneratePalette(tt.base, tt.opts)
|
||||
|
||||
if len(result) != 16 {
|
||||
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
|
||||
}
|
||||
|
||||
for i, color := range result {
|
||||
if len(color) != 7 || color[0] != '#' {
|
||||
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
|
||||
}
|
||||
}
|
||||
|
||||
if tt.opts.Background != "" && result[0] != tt.opts.Background {
|
||||
t.Errorf("Background color = %s, expected %s", result[0], tt.opts.Background)
|
||||
} else if !tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#1a1a1a" {
|
||||
t.Errorf("Dark mode background = %s, expected #1a1a1a", result[0])
|
||||
} else if tt.opts.IsLight && tt.opts.Background == "" && result[0] != "#f8f8f8" {
|
||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result[0])
|
||||
}
|
||||
|
||||
if tt.opts.IsLight && result[15] != "#1a1a1a" {
|
||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result[15])
|
||||
} else if !tt.opts.IsLight && result[15] != "#ffffff" {
|
||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result[15])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichVSCodeTheme(t *testing.T) {
|
||||
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
|
||||
|
||||
baseTheme := map[string]interface{}{
|
||||
"name": "Test Theme",
|
||||
"type": "dark",
|
||||
"colors": map[string]interface{}{
|
||||
"editor.background": "#000000",
|
||||
},
|
||||
}
|
||||
|
||||
themeJSON, err := json.Marshal(baseTheme)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal base theme: %v", err)
|
||||
}
|
||||
|
||||
result, err := EnrichVSCodeTheme(themeJSON, colors)
|
||||
if err != nil {
|
||||
t.Fatalf("EnrichVSCodeTheme failed: %v", err)
|
||||
}
|
||||
|
||||
var enriched map[string]interface{}
|
||||
if err := json.Unmarshal(result, &enriched); err != nil {
|
||||
t.Fatalf("Failed to unmarshal result: %v", err)
|
||||
}
|
||||
|
||||
colorsMap, ok := enriched["colors"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("colors is not a map")
|
||||
}
|
||||
|
||||
terminalColors := []string{
|
||||
"terminal.ansiBlack",
|
||||
"terminal.ansiRed",
|
||||
"terminal.ansiGreen",
|
||||
"terminal.ansiYellow",
|
||||
"terminal.ansiBlue",
|
||||
"terminal.ansiMagenta",
|
||||
"terminal.ansiCyan",
|
||||
"terminal.ansiWhite",
|
||||
"terminal.ansiBrightBlack",
|
||||
"terminal.ansiBrightRed",
|
||||
"terminal.ansiBrightGreen",
|
||||
"terminal.ansiBrightYellow",
|
||||
"terminal.ansiBrightBlue",
|
||||
"terminal.ansiBrightMagenta",
|
||||
"terminal.ansiBrightCyan",
|
||||
"terminal.ansiBrightWhite",
|
||||
}
|
||||
|
||||
for i, key := range terminalColors {
|
||||
if val, ok := colorsMap[key]; !ok {
|
||||
t.Errorf("Missing terminal color: %s", key)
|
||||
} else if val != colors[i] {
|
||||
t.Errorf("%s = %s, expected %s", key, val, colors[i])
|
||||
}
|
||||
}
|
||||
|
||||
if colorsMap["editor.background"] != "#000000" {
|
||||
t.Error("Original theme colors should be preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichVSCodeThemeInvalidJSON(t *testing.T) {
|
||||
colors := GeneratePalette("#625690", PaletteOptions{IsLight: false})
|
||||
invalidJSON := []byte("{invalid json")
|
||||
|
||||
_, err := EnrichVSCodeTheme(invalidJSON, colors)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTripConversion(t *testing.T) {
|
||||
testColors := []string{"#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff", "#625690", "#808080"}
|
||||
|
||||
for _, hex := range testColors {
|
||||
t.Run(hex, func(t *testing.T) {
|
||||
rgb := HexToRGB(hex)
|
||||
result := RGBToHex(rgb)
|
||||
if result != hex {
|
||||
t.Errorf("Round trip %s -> RGB -> %s failed", hex, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRGBHSVRoundTrip(t *testing.T) {
|
||||
testCases := []RGB{
|
||||
{R: 0.0, G: 0.0, B: 0.0},
|
||||
{R: 1.0, G: 1.0, B: 1.0},
|
||||
{R: 1.0, G: 0.0, B: 0.0},
|
||||
{R: 0.0, G: 1.0, B: 0.0},
|
||||
{R: 0.0, G: 0.0, B: 1.0},
|
||||
{R: 0.5, G: 0.5, B: 0.5},
|
||||
{R: 0.3843137254901961, G: 0.33725490196078434, B: 0.5647058823529412},
|
||||
}
|
||||
|
||||
for _, rgb := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
hsv := RGBToHSV(rgb)
|
||||
result := HSVToRGB(hsv)
|
||||
if !floatEqual(result.R, rgb.R) || !floatEqual(result.G, rgb.G) || !floatEqual(result.B, rgb.B) {
|
||||
t.Errorf("Round trip RGB->HSV->RGB failed: %v -> %v -> %v", rgb, hsv, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func floatEqual(a, b float64) bool {
|
||||
return math.Abs(a-b) < 1e-9
|
||||
}
|
||||
|
||||
func TestDeltaPhiStar(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fg string
|
||||
bg string
|
||||
negativePolarity bool
|
||||
minExpected float64
|
||||
}{
|
||||
{
|
||||
name: "white on black (negative polarity)",
|
||||
fg: "#ffffff",
|
||||
bg: "#000000",
|
||||
negativePolarity: true,
|
||||
minExpected: 100.0,
|
||||
},
|
||||
{
|
||||
name: "black on white (positive polarity)",
|
||||
fg: "#000000",
|
||||
bg: "#ffffff",
|
||||
negativePolarity: false,
|
||||
minExpected: 100.0,
|
||||
},
|
||||
{
|
||||
name: "low contrast same color",
|
||||
fg: "#808080",
|
||||
bg: "#808080",
|
||||
negativePolarity: false,
|
||||
minExpected: -40.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := DeltaPhiStar(tt.fg, tt.bg, tt.negativePolarity)
|
||||
if result < tt.minExpected {
|
||||
t.Errorf("DeltaPhiStar(%s, %s, %v) = %f, expected >= %f",
|
||||
tt.fg, tt.bg, tt.negativePolarity, result, tt.minExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeltaPhiStarContrast(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fg string
|
||||
bg string
|
||||
isLightMode bool
|
||||
minExpected float64
|
||||
}{
|
||||
{
|
||||
name: "white on black (dark mode)",
|
||||
fg: "#ffffff",
|
||||
bg: "#000000",
|
||||
isLightMode: false,
|
||||
minExpected: 100.0,
|
||||
},
|
||||
{
|
||||
name: "black on white (light mode)",
|
||||
fg: "#000000",
|
||||
bg: "#ffffff",
|
||||
isLightMode: true,
|
||||
minExpected: 100.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := DeltaPhiStarContrast(tt.fg, tt.bg, tt.isLightMode)
|
||||
if result < tt.minExpected {
|
||||
t.Errorf("DeltaPhiStarContrast(%s, %s, %v) = %f, expected >= %f",
|
||||
tt.fg, tt.bg, tt.isLightMode, result, tt.minExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureContrastDPS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
color string
|
||||
bg string
|
||||
minLc float64
|
||||
isLightMode bool
|
||||
}{
|
||||
{
|
||||
name: "already sufficient contrast dark mode",
|
||||
color: "#ffffff",
|
||||
bg: "#000000",
|
||||
minLc: 60.0,
|
||||
isLightMode: false,
|
||||
},
|
||||
{
|
||||
name: "already sufficient contrast light mode",
|
||||
color: "#000000",
|
||||
bg: "#ffffff",
|
||||
minLc: 60.0,
|
||||
isLightMode: true,
|
||||
},
|
||||
{
|
||||
name: "needs adjustment dark mode",
|
||||
color: "#404040",
|
||||
bg: "#1a1a1a",
|
||||
minLc: 60.0,
|
||||
isLightMode: false,
|
||||
},
|
||||
{
|
||||
name: "needs adjustment light mode",
|
||||
color: "#c0c0c0",
|
||||
bg: "#f8f8f8",
|
||||
minLc: 60.0,
|
||||
isLightMode: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := EnsureContrastDPS(tt.color, tt.bg, tt.minLc, tt.isLightMode)
|
||||
actualLc := DeltaPhiStarContrast(result, tt.bg, tt.isLightMode)
|
||||
if actualLc < tt.minLc {
|
||||
t.Errorf("EnsureContrastDPS(%s, %s, %f, %t) = %s with Lc %f, expected Lc >= %f",
|
||||
tt.color, tt.bg, tt.minLc, tt.isLightMode, result, actualLc, tt.minLc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteWithDPS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
base string
|
||||
opts PaletteOptions
|
||||
}{
|
||||
{
|
||||
name: "dark theme with DPS",
|
||||
base: "#625690",
|
||||
opts: PaletteOptions{IsLight: false, UseDPS: true},
|
||||
},
|
||||
{
|
||||
name: "light theme with DPS",
|
||||
base: "#625690",
|
||||
opts: PaletteOptions{IsLight: true, UseDPS: true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GeneratePalette(tt.base, tt.opts)
|
||||
|
||||
if len(result) != 16 {
|
||||
t.Errorf("GeneratePalette returned %d colors, expected 16", len(result))
|
||||
}
|
||||
|
||||
for i, color := range result {
|
||||
if len(color) != 7 || color[0] != '#' {
|
||||
t.Errorf("Color at index %d (%s) is not a valid hex color", i, color)
|
||||
}
|
||||
}
|
||||
|
||||
bgColor := result[0]
|
||||
for i := 1; i < 8; i++ {
|
||||
lc := DeltaPhiStarContrast(result[i], bgColor, tt.opts.IsLight)
|
||||
minLc := 30.0
|
||||
if lc < minLc && lc > 0 {
|
||||
t.Errorf("Color %d (%s) has insufficient DPS contrast %f with background %s (expected >= %f)",
|
||||
i, result[i], lc, bgColor, minLc)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveContainer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
primary string
|
||||
isLight bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "dark mode",
|
||||
primary: "#ccbdff",
|
||||
isLight: false,
|
||||
expected: "#4a3e76",
|
||||
},
|
||||
{
|
||||
name: "light mode",
|
||||
primary: "#625690",
|
||||
isLight: true,
|
||||
expected: "#e7deff",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := DeriveContainer(tt.primary, tt.isLight)
|
||||
|
||||
resultRGB := HexToRGB(result)
|
||||
expectedRGB := HexToRGB(tt.expected)
|
||||
|
||||
rDiff := math.Abs(resultRGB.R - expectedRGB.R)
|
||||
gDiff := math.Abs(resultRGB.G - expectedRGB.G)
|
||||
bDiff := math.Abs(resultRGB.B - expectedRGB.B)
|
||||
|
||||
tolerance := 0.02
|
||||
if rDiff > tolerance || gDiff > tolerance || bDiff > tolerance {
|
||||
t.Errorf("DeriveContainer(%s, %v) = %s, expected %s (RGB diff: R:%.4f G:%.4f B:%.4f)",
|
||||
tt.primary, tt.isLight, result, tt.expected, rDiff, gDiff, bDiff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrastAlgorithmComparison(t *testing.T) {
|
||||
base := "#625690"
|
||||
|
||||
optsWCAG := PaletteOptions{IsLight: false, UseDPS: false}
|
||||
optsDPS := PaletteOptions{IsLight: false, UseDPS: true}
|
||||
|
||||
paletteWCAG := GeneratePalette(base, optsWCAG)
|
||||
paletteDPS := GeneratePalette(base, optsDPS)
|
||||
|
||||
if len(paletteWCAG) != 16 || len(paletteDPS) != 16 {
|
||||
t.Fatal("Both palettes should have 16 colors")
|
||||
}
|
||||
|
||||
if paletteWCAG[0] != paletteDPS[0] {
|
||||
t.Errorf("Background colors differ: WCAG=%s, DPS=%s", paletteWCAG[0], paletteDPS[0])
|
||||
}
|
||||
|
||||
differentCount := 0
|
||||
for i := 0; i < 16; i++ {
|
||||
if paletteWCAG[i] != paletteDPS[i] {
|
||||
differentCount++
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
|
||||
}
|
||||
126
internal/dank16/terminals.go
Normal file
126
internal/dank16/terminals.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package dank16
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GenerateJSON(colors []string) string {
|
||||
colorMap := make(map[string]string)
|
||||
|
||||
for i, color := range colors {
|
||||
colorMap[fmt.Sprintf("color%d", i)] = color
|
||||
}
|
||||
|
||||
marshalled, _ := json.Marshal(colorMap)
|
||||
|
||||
return string(marshalled)
|
||||
}
|
||||
|
||||
func GenerateKittyTheme(colors []string) string {
|
||||
kittyColors := []struct {
|
||||
name string
|
||||
index int
|
||||
}{
|
||||
{"color0", 0},
|
||||
{"color1", 1},
|
||||
{"color2", 2},
|
||||
{"color3", 3},
|
||||
{"color4", 4},
|
||||
{"color5", 5},
|
||||
{"color6", 6},
|
||||
{"color7", 7},
|
||||
{"color8", 8},
|
||||
{"color9", 9},
|
||||
{"color10", 10},
|
||||
{"color11", 11},
|
||||
{"color12", 12},
|
||||
{"color13", 13},
|
||||
{"color14", 14},
|
||||
{"color15", 15},
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for _, kc := range kittyColors {
|
||||
fmt.Fprintf(&result, "%s %s\n", kc.name, colors[kc.index])
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func GenerateFootTheme(colors []string) string {
|
||||
footColors := []struct {
|
||||
name string
|
||||
index int
|
||||
}{
|
||||
{"regular0", 0},
|
||||
{"regular1", 1},
|
||||
{"regular2", 2},
|
||||
{"regular3", 3},
|
||||
{"regular4", 4},
|
||||
{"regular5", 5},
|
||||
{"regular6", 6},
|
||||
{"regular7", 7},
|
||||
{"bright0", 8},
|
||||
{"bright1", 9},
|
||||
{"bright2", 10},
|
||||
{"bright3", 11},
|
||||
{"bright4", 12},
|
||||
{"bright5", 13},
|
||||
{"bright6", 14},
|
||||
{"bright7", 15},
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for _, fc := range footColors {
|
||||
fmt.Fprintf(&result, "%s=%s\n", fc.name, strings.TrimPrefix(colors[fc.index], "#"))
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func GenerateAlacrittyTheme(colors []string) string {
|
||||
alacrittyColors := []struct {
|
||||
section string
|
||||
name string
|
||||
index int
|
||||
}{
|
||||
{"normal", "black", 0},
|
||||
{"normal", "red", 1},
|
||||
{"normal", "green", 2},
|
||||
{"normal", "yellow", 3},
|
||||
{"normal", "blue", 4},
|
||||
{"normal", "magenta", 5},
|
||||
{"normal", "cyan", 6},
|
||||
{"normal", "white", 7},
|
||||
{"bright", "black", 8},
|
||||
{"bright", "red", 9},
|
||||
{"bright", "green", 10},
|
||||
{"bright", "yellow", 11},
|
||||
{"bright", "blue", 12},
|
||||
{"bright", "magenta", 13},
|
||||
{"bright", "cyan", 14},
|
||||
{"bright", "white", 15},
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
currentSection := ""
|
||||
for _, ac := range alacrittyColors {
|
||||
if ac.section != currentSection {
|
||||
if currentSection != "" {
|
||||
result.WriteString("\n")
|
||||
}
|
||||
fmt.Fprintf(&result, "[colors.%s]\n", ac.section)
|
||||
currentSection = ac.section
|
||||
}
|
||||
fmt.Fprintf(&result, "%-7s = '%s'\n", ac.name, colors[ac.index])
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func GenerateGhosttyTheme(colors []string) string {
|
||||
var result strings.Builder
|
||||
for i, color := range colors {
|
||||
fmt.Fprintf(&result, "palette = %d=%s\n", i, color)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
250
internal/dank16/vscode.go
Normal file
250
internal/dank16/vscode.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package dank16
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type VSCodeTheme struct {
|
||||
Schema string `json:"$schema"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Colors map[string]string `json:"colors"`
|
||||
TokenColors []VSCodeTokenColor `json:"tokenColors"`
|
||||
SemanticHighlighting bool `json:"semanticHighlighting"`
|
||||
SemanticTokenColors map[string]VSCodeTokenSetting `json:"semanticTokenColors"`
|
||||
}
|
||||
|
||||
type VSCodeTokenColor struct {
|
||||
Scope interface{} `json:"scope"`
|
||||
Settings VSCodeTokenSetting `json:"settings"`
|
||||
}
|
||||
|
||||
type VSCodeTokenSetting struct {
|
||||
Foreground string `json:"foreground,omitempty"`
|
||||
FontStyle string `json:"fontStyle,omitempty"`
|
||||
}
|
||||
|
||||
func updateTokenColor(tc interface{}, scopeToColor map[string]string) {
|
||||
tcMap, ok := tc.(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
scopes, ok := tcMap["scope"].([]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
settings, ok := tcMap["settings"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
isYaml := hasScopeContaining(scopes, "yaml")
|
||||
|
||||
for _, scope := range scopes {
|
||||
scopeStr, ok := scope.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if scopeStr == "string" && isYaml {
|
||||
continue
|
||||
}
|
||||
|
||||
if applyColorToScope(settings, scope, scopeToColor) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyColorToScope(settings map[string]interface{}, scope interface{}, scopeToColor map[string]string) bool {
|
||||
scopeStr, ok := scope.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
newColor, exists := scopeToColor[scopeStr]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
settings["foreground"] = newColor
|
||||
return true
|
||||
}
|
||||
|
||||
func hasScopeContaining(scopes []interface{}, substring string) bool {
|
||||
for _, scope := range scopes {
|
||||
scopeStr, ok := scope.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i <= len(scopeStr)-len(substring); i++ {
|
||||
if scopeStr[i:i+len(substring)] == substring {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func EnrichVSCodeTheme(themeData []byte, colors []string) ([]byte, error) {
|
||||
var theme map[string]interface{}
|
||||
if err := json.Unmarshal(themeData, &theme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
colorsMap, ok := theme["colors"].(map[string]interface{})
|
||||
if !ok {
|
||||
colorsMap = make(map[string]interface{})
|
||||
theme["colors"] = colorsMap
|
||||
}
|
||||
|
||||
bg := colors[0]
|
||||
isLight := false
|
||||
if len(bg) == 7 && bg[0] == '#' {
|
||||
r, g, b := 0, 0, 0
|
||||
fmt.Sscanf(bg[1:], "%02x%02x%02x", &r, &g, &b)
|
||||
luminance := (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255.0
|
||||
isLight = luminance > 0.5
|
||||
}
|
||||
|
||||
if isLight {
|
||||
theme["type"] = "light"
|
||||
} else {
|
||||
theme["type"] = "dark"
|
||||
}
|
||||
|
||||
colorsMap["terminal.ansiBlack"] = colors[0]
|
||||
colorsMap["terminal.ansiRed"] = colors[1]
|
||||
colorsMap["terminal.ansiGreen"] = colors[2]
|
||||
colorsMap["terminal.ansiYellow"] = colors[3]
|
||||
colorsMap["terminal.ansiBlue"] = colors[4]
|
||||
colorsMap["terminal.ansiMagenta"] = colors[5]
|
||||
colorsMap["terminal.ansiCyan"] = colors[6]
|
||||
colorsMap["terminal.ansiWhite"] = colors[7]
|
||||
colorsMap["terminal.ansiBrightBlack"] = colors[8]
|
||||
colorsMap["terminal.ansiBrightRed"] = colors[9]
|
||||
colorsMap["terminal.ansiBrightGreen"] = colors[10]
|
||||
colorsMap["terminal.ansiBrightYellow"] = colors[11]
|
||||
colorsMap["terminal.ansiBrightBlue"] = colors[12]
|
||||
colorsMap["terminal.ansiBrightMagenta"] = colors[13]
|
||||
colorsMap["terminal.ansiBrightCyan"] = colors[14]
|
||||
colorsMap["terminal.ansiBrightWhite"] = colors[15]
|
||||
|
||||
tokenColors, ok := theme["tokenColors"].([]interface{})
|
||||
if ok {
|
||||
scopeToColor := map[string]string{
|
||||
"comment": colors[8],
|
||||
"punctuation.definition.comment": colors[8],
|
||||
"keyword": colors[5],
|
||||
"storage.type": colors[13],
|
||||
"storage.modifier": colors[5],
|
||||
"variable": colors[15],
|
||||
"variable.parameter": colors[7],
|
||||
"meta.object-literal.key": colors[4],
|
||||
"meta.property.object": colors[4],
|
||||
"variable.other.property": colors[4],
|
||||
"constant.other.symbol": colors[12],
|
||||
"constant.numeric": colors[12],
|
||||
"constant.language": colors[12],
|
||||
"constant.character": colors[3],
|
||||
"entity.name.type": colors[12],
|
||||
"support.type": colors[13],
|
||||
"entity.name.class": colors[12],
|
||||
"entity.name.function": colors[2],
|
||||
"support.function": colors[2],
|
||||
"support.class": colors[15],
|
||||
"support.variable": colors[15],
|
||||
"variable.language": colors[12],
|
||||
"entity.name.tag.yaml": colors[12],
|
||||
"string.unquoted.plain.out.yaml": colors[15],
|
||||
"string.unquoted.yaml": colors[15],
|
||||
"string": colors[3],
|
||||
}
|
||||
|
||||
for i, tc := range tokenColors {
|
||||
updateTokenColor(tc, scopeToColor)
|
||||
tokenColors[i] = tc
|
||||
}
|
||||
|
||||
yamlRules := []VSCodeTokenColor{
|
||||
{
|
||||
Scope: "entity.name.tag.yaml",
|
||||
Settings: VSCodeTokenSetting{Foreground: colors[12]},
|
||||
},
|
||||
{
|
||||
Scope: []string{"string.unquoted.plain.out.yaml", "string.unquoted.yaml"},
|
||||
Settings: VSCodeTokenSetting{Foreground: colors[15]},
|
||||
},
|
||||
}
|
||||
|
||||
for _, rule := range yamlRules {
|
||||
tokenColors = append(tokenColors, rule)
|
||||
}
|
||||
|
||||
theme["tokenColors"] = tokenColors
|
||||
}
|
||||
|
||||
if semanticTokenColors, ok := theme["semanticTokenColors"].(map[string]interface{}); ok {
|
||||
updates := map[string]string{
|
||||
"variable": colors[15],
|
||||
"variable.readonly": colors[12],
|
||||
"property": colors[4],
|
||||
"function": colors[2],
|
||||
"method": colors[2],
|
||||
"type": colors[12],
|
||||
"class": colors[12],
|
||||
"typeParameter": colors[13],
|
||||
"enumMember": colors[12],
|
||||
"string": colors[3],
|
||||
"number": colors[12],
|
||||
"comment": colors[8],
|
||||
"keyword": colors[5],
|
||||
"operator": colors[15],
|
||||
"parameter": colors[7],
|
||||
"namespace": colors[15],
|
||||
}
|
||||
|
||||
for key, color := range updates {
|
||||
if existing, ok := semanticTokenColors[key].(map[string]interface{}); ok {
|
||||
existing["foreground"] = color
|
||||
} else {
|
||||
semanticTokenColors[key] = map[string]interface{}{
|
||||
"foreground": color,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
semanticTokenColors := make(map[string]interface{})
|
||||
updates := map[string]string{
|
||||
"variable": colors[7],
|
||||
"variable.readonly": colors[12],
|
||||
"property": colors[4],
|
||||
"function": colors[2],
|
||||
"method": colors[2],
|
||||
"type": colors[12],
|
||||
"class": colors[12],
|
||||
"typeParameter": colors[13],
|
||||
"enumMember": colors[12],
|
||||
"string": colors[3],
|
||||
"number": colors[12],
|
||||
"comment": colors[8],
|
||||
"keyword": colors[5],
|
||||
"operator": colors[15],
|
||||
"parameter": colors[7],
|
||||
"namespace": colors[15],
|
||||
}
|
||||
|
||||
for key, color := range updates {
|
||||
semanticTokenColors[key] = map[string]interface{}{
|
||||
"foreground": color,
|
||||
}
|
||||
}
|
||||
theme["semanticTokenColors"] = semanticTokenColors
|
||||
}
|
||||
|
||||
return json.MarshalIndent(theme, "", " ")
|
||||
}
|
||||
51
internal/deps/detector.go
Normal file
51
internal/deps/detector.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package deps
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type DependencyStatus int
|
||||
|
||||
const (
|
||||
StatusMissing DependencyStatus = iota
|
||||
StatusInstalled
|
||||
StatusNeedsUpdate
|
||||
StatusNeedsReinstall
|
||||
)
|
||||
|
||||
type PackageVariant int
|
||||
|
||||
const (
|
||||
VariantStable PackageVariant = iota
|
||||
VariantGit
|
||||
)
|
||||
|
||||
type Dependency struct {
|
||||
Name string
|
||||
Status DependencyStatus
|
||||
Version string
|
||||
Description string
|
||||
Required bool
|
||||
Variant PackageVariant
|
||||
CanToggle bool
|
||||
}
|
||||
|
||||
type WindowManager int
|
||||
|
||||
const (
|
||||
WindowManagerHyprland WindowManager = iota
|
||||
WindowManagerNiri
|
||||
)
|
||||
|
||||
type Terminal int
|
||||
|
||||
const (
|
||||
TerminalGhostty Terminal = iota
|
||||
TerminalKitty
|
||||
TerminalAlacritty
|
||||
)
|
||||
|
||||
type DependencyDetector interface {
|
||||
DetectDependencies(ctx context.Context, wm WindowManager) ([]Dependency, error)
|
||||
DetectDependenciesWithTerminal(ctx context.Context, wm WindowManager, terminal Terminal) ([]Dependency, error)
|
||||
}
|
||||
1000
internal/distros/arch.go
Normal file
1000
internal/distros/arch.go
Normal file
File diff suppressed because it is too large
Load Diff
668
internal/distros/base.go
Normal file
668
internal/distros/base.go
Normal file
@@ -0,0 +1,668 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
"github.com/AvengeMedia/danklinux/internal/version"
|
||||
)
|
||||
|
||||
const forceQuickshellGit = false
|
||||
const forceDMSGit = false
|
||||
|
||||
// BaseDistribution provides common functionality for all distributions
|
||||
type BaseDistribution struct {
|
||||
logChan chan<- string
|
||||
}
|
||||
|
||||
// NewBaseDistribution creates a new base distribution
|
||||
func NewBaseDistribution(logChan chan<- string) *BaseDistribution {
|
||||
return &BaseDistribution{
|
||||
logChan: logChan,
|
||||
}
|
||||
}
|
||||
|
||||
// Common helper methods
|
||||
func (b *BaseDistribution) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) CommandExists(cmd string) bool {
|
||||
return b.commandExists(cmd)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) log(message string) {
|
||||
if b.logChan != nil {
|
||||
b.logChan <- message
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) logError(message string, err error) {
|
||||
errorMsg := fmt.Sprintf("ERROR: %s: %v", message, err)
|
||||
b.log(errorMsg)
|
||||
}
|
||||
|
||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
||||
func escapeSingleQuotes(s string) string {
|
||||
return strings.ReplaceAll(s, "'", "'\\''")
|
||||
}
|
||||
|
||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
||||
// This helper escapes special characters in the password to prevent shell injection
|
||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
||||
}
|
||||
|
||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||
}
|
||||
|
||||
// Keep unexported versions for backward compatibility within package
|
||||
func makeSudoCommand(sudoPassword string, command string) string {
|
||||
return MakeSudoCommand(sudoPassword, command)
|
||||
}
|
||||
|
||||
func execSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
||||
return ExecSudoCommand(ctx, sudoPassword, command)
|
||||
}
|
||||
|
||||
// Common dependency detection methods
|
||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("git") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "git",
|
||||
Status: status,
|
||||
Description: "Version control system",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("matugen") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "matugen",
|
||||
Status: status,
|
||||
Description: "Material Design color generation tool",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("dgop") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "dgop",
|
||||
Status: status,
|
||||
Description: "Desktop portal management tool",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||
|
||||
status := deps.StatusMissing
|
||||
currentVersion := ""
|
||||
|
||||
if _, err := os.Stat(dmsPath); err == nil {
|
||||
status = deps.StatusInstalled
|
||||
|
||||
// Only get current version, don't check for updates (lazy loading)
|
||||
current, err := version.GetCurrentDMSVersion()
|
||||
if err == nil {
|
||||
currentVersion = current
|
||||
}
|
||||
}
|
||||
|
||||
dep := deps.Dependency{
|
||||
Name: "dms (DankMaterialShell)",
|
||||
Status: status,
|
||||
Description: "Desktop Management System configuration",
|
||||
Required: true,
|
||||
CanToggle: true,
|
||||
}
|
||||
|
||||
if currentVersion != "" {
|
||||
dep.Version = currentVersion
|
||||
}
|
||||
|
||||
return dep
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.Dependency {
|
||||
switch terminal {
|
||||
case deps.TerminalGhostty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("ghostty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "ghostty",
|
||||
Status: status,
|
||||
Description: "A fast, native terminal emulator built in Zig.",
|
||||
Required: true,
|
||||
}
|
||||
case deps.TerminalKitty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("kitty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "kitty",
|
||||
Status: status,
|
||||
Description: "A feature-rich, customizable terminal emulator.",
|
||||
Required: true,
|
||||
}
|
||||
case deps.TerminalAlacritty:
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("alacritty") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "alacritty",
|
||||
Status: status,
|
||||
Description: "A simple terminal emulator. (No dynamic theming)",
|
||||
Required: true,
|
||||
}
|
||||
default:
|
||||
return b.detectSpecificTerminal(deps.TerminalGhostty)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
cliphist := deps.StatusMissing
|
||||
if b.commandExists("cliphist") {
|
||||
cliphist = deps.StatusInstalled
|
||||
}
|
||||
|
||||
wlClipboard := deps.StatusMissing
|
||||
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
||||
wlClipboard = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies,
|
||||
deps.Dependency{
|
||||
Name: "cliphist",
|
||||
Status: cliphist,
|
||||
Description: "Wayland clipboard manager",
|
||||
Required: true,
|
||||
},
|
||||
deps.Dependency{
|
||||
Name: "wl-clipboard",
|
||||
Status: wlClipboard,
|
||||
Description: "Wayland clipboard utilities",
|
||||
Required: true,
|
||||
},
|
||||
)
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectHyprpicker() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists("hyprpicker") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "hyprpicker",
|
||||
Status: status,
|
||||
Description: "Color picker for Wayland",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
tools := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"grim", "Screenshot utility for Wayland"},
|
||||
{"slurp", "Region selection utility for Wayland"},
|
||||
{"hyprctl", "Hyprland control utility"},
|
||||
{"grimblast", "Screenshot script for Hyprland"},
|
||||
{"jq", "JSON processor"},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
status := deps.StatusMissing
|
||||
if b.commandExists(tool.name) {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, deps.Dependency{
|
||||
Name: tool.name,
|
||||
Status: status,
|
||||
Description: tool.description,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectQuickshell() deps.Dependency {
|
||||
if !b.commandExists("qs") {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "QtQuick based desktop shell toolkit",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("qs", "--version")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsReinstall,
|
||||
Description: "QtQuick based desktop shell toolkit (version check failed)",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
versionStr := string(output)
|
||||
versionRegex := regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
||||
matches := versionRegex.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) < 2 {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsReinstall,
|
||||
Description: "QtQuick based desktop shell toolkit (unknown version)",
|
||||
Required: true,
|
||||
Variant: deps.VariantStable,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
version := matches[1]
|
||||
variant := deps.VariantStable
|
||||
if strings.Contains(versionStr, "git") || strings.Contains(versionStr, "+") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
|
||||
if b.versionCompare(version, "0.2.0") >= 0 {
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusInstalled,
|
||||
Version: version,
|
||||
Description: "QtQuick based desktop shell toolkit",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "quickshell",
|
||||
Status: deps.StatusNeedsUpdate,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
Version: version,
|
||||
Description: "QtQuick based desktop shell toolkit (needs 0.2.0+)",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("hyprland") || b.commandExists("Hyprland") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("hyprctl", "version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`v(\d+\.\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "hyprland",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Dynamic tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
case deps.WindowManagerNiri:
|
||||
status := deps.StatusMissing
|
||||
variant := deps.VariantStable
|
||||
version := ""
|
||||
|
||||
if b.commandExists("niri") {
|
||||
status = deps.StatusInstalled
|
||||
cmd := exec.Command("niri", "--version")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
outStr := string(output)
|
||||
if strings.Contains(outStr, "git") || strings.Contains(outStr, "+") {
|
||||
variant = deps.VariantGit
|
||||
}
|
||||
if versionRegex := regexp.MustCompile(`niri (\d+\.\d+)`); versionRegex.MatchString(outStr) {
|
||||
matches := versionRegex.FindStringSubmatch(outStr)
|
||||
if len(matches) > 1 {
|
||||
version = matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "niri",
|
||||
Status: status,
|
||||
Version: version,
|
||||
Description: "Scrollable-tiling Wayland compositor",
|
||||
Required: true,
|
||||
Variant: variant,
|
||||
CanToggle: true,
|
||||
}
|
||||
default:
|
||||
return deps.Dependency{
|
||||
Name: "unknown-wm",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "Unknown window manager",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version comparison helper
|
||||
func (b *BaseDistribution) versionCompare(v1, v2 string) int {
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
||||
if parts1[i] < parts2[i] {
|
||||
return -1
|
||||
}
|
||||
if parts1[i] > parts2[i] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts1) < len(parts2) {
|
||||
return -1
|
||||
}
|
||||
if len(parts1) > len(parts2) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Common installation helper
|
||||
func (b *BaseDistribution) runWithProgress(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64) error {
|
||||
return b.runWithProgressTimeout(cmd, progressChan, phase, startProgress, endProgress, 20*time.Minute)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, timeout time.Duration) error {
|
||||
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, "Installing...", timeout)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressStep(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string) error {
|
||||
return b.runWithProgressStepTimeout(cmd, progressChan, phase, startProgress, endProgress, stepMessage, 20*time.Minute)
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressChan chan<- InstallProgressMsg, phase InstallPhase, startProgress, endProgress float64, stepMessage string, timeoutDuration time.Duration) error {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputChan := make(chan string, 100)
|
||||
done := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
b.log(line)
|
||||
outputChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
b.log(line)
|
||||
outputChan <- line
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
close(outputChan)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
progress := startProgress
|
||||
progressStep := (endProgress - startProgress) / 50
|
||||
lastOutput := ""
|
||||
|
||||
var timeout *time.Timer
|
||||
var timeoutChan <-chan time.Time
|
||||
if timeoutDuration > 0 {
|
||||
timeout = time.NewTimer(timeoutDuration)
|
||||
defer timeout.Stop()
|
||||
timeoutChan = timeout.C
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
b.logError("Command execution failed", err)
|
||||
b.log(fmt.Sprintf("Last output before failure: %s", lastOutput))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: startProgress,
|
||||
Step: "Command failed",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
}
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: endProgress,
|
||||
Step: "Installation step complete",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
return nil
|
||||
case output, ok := <-outputChan:
|
||||
if ok {
|
||||
lastOutput = output
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: stepMessage,
|
||||
IsComplete: false,
|
||||
LogOutput: output,
|
||||
}
|
||||
if timeout != nil {
|
||||
timeout.Reset(timeoutDuration)
|
||||
}
|
||||
}
|
||||
case <-timeoutChan:
|
||||
if cmd.Process != nil {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
err := fmt.Errorf("installation timed out after %v", timeoutDuration)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: startProgress,
|
||||
Step: "Installation timed out",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
Error: err,
|
||||
}
|
||||
return err
|
||||
case <-ticker.C:
|
||||
if progress < endProgress-0.01 {
|
||||
progress += progressStep
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: "Installing...",
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// installDMSBinary installs the DMS binary from GitHub releases
|
||||
func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
b.log("Installing/updating DMS binary...")
|
||||
|
||||
// Detect architecture
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
case "amd64":
|
||||
case "arm64":
|
||||
default:
|
||||
return fmt.Errorf("unsupported architecture for DMS: %s", arch)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.80,
|
||||
Step: "Downloading DMS binary...",
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("Downloading dms-%s.gz", arch),
|
||||
}
|
||||
|
||||
// Get latest release version
|
||||
latestVersionCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
`curl -s https://api.github.com/repos/AvengeMedia/danklinux/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'`)
|
||||
versionOutput, err := latestVersionCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest DMS version: %w", err)
|
||||
}
|
||||
version := strings.TrimSpace(string(versionOutput))
|
||||
if version == "" {
|
||||
return fmt.Errorf("could not determine latest DMS version")
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "manual-builds")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Download the gzipped binary
|
||||
downloadURL := fmt.Sprintf("https://github.com/AvengeMedia/danklinux/releases/download/%s/dms-%s.gz", version, arch)
|
||||
gzPath := filepath.Join(tmpDir, "dms.gz")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", downloadURL, "-o", gzPath)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to download DMS binary: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.85,
|
||||
Step: "Extracting DMS binary...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "gunzip dms.gz",
|
||||
}
|
||||
|
||||
// Extract the binary
|
||||
extractCmd := exec.CommandContext(ctx, "gunzip", gzPath)
|
||||
if err := extractCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to extract DMS binary: %w", err)
|
||||
}
|
||||
|
||||
binaryPath := filepath.Join(tmpDir, "dms")
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", binaryPath)
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make DMS binary executable: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.88,
|
||||
Step: "Installing DMS to /usr/local/bin...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp dms /usr/local/bin/",
|
||||
}
|
||||
|
||||
// Install to /usr/local/bin
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||
}
|
||||
|
||||
b.log("DMS binary installed successfully")
|
||||
return nil
|
||||
}
|
||||
220
internal/distros/base_test.go
Normal file
220
internal/distros/base_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Status != deps.StatusMissing {
|
||||
t.Errorf("Expected StatusMissing, got %d", dep.Status)
|
||||
}
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
||||
if !commandExists("git") {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
exec.Command("git", "init", dmsPath).Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||
|
||||
testFile := filepath.Join(dmsPath, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test"), 0644)
|
||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Status == deps.StatusMissing {
|
||||
t.Error("Expected DMS to be detected as installed")
|
||||
}
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
|
||||
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
|
||||
}
|
||||
|
||||
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
||||
if !commandExists("git") {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
exec.Command("git", "init", dmsPath).Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.email", "test@test.com").Run()
|
||||
exec.Command("git", "-C", dmsPath, "config", "user.name", "Test User").Run()
|
||||
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
||||
|
||||
testFile := filepath.Join(dmsPath, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test"), 0644)
|
||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
||||
exec.Command("git", "-C", dmsPath, "checkout", "v0.0.1").Run()
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
|
||||
t.Logf("Status: %d, Version: %s", dep.Status, dep.Version)
|
||||
}
|
||||
|
||||
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
os.Setenv("HOME", tempDir)
|
||||
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
dep := base.detectDMS()
|
||||
|
||||
if dep.Status == deps.StatusMissing {
|
||||
t.Error("Expected DMS to be detected as present")
|
||||
}
|
||||
|
||||
if dep.Name != "dms (DankMaterialShell)" {
|
||||
t.Errorf("Expected name 'dms (DankMaterialShell)', got %s", dep.Name)
|
||||
}
|
||||
|
||||
if !dep.Required {
|
||||
t.Error("Expected Required to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
|
||||
if base == nil {
|
||||
t.Fatal("NewBaseDistribution returned nil")
|
||||
}
|
||||
|
||||
if base.logChan == nil {
|
||||
t.Error("logChan was not set")
|
||||
}
|
||||
}
|
||||
|
||||
func commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func TestBaseDistribution_versionCompare(t *testing.T) {
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
|
||||
tests := []struct {
|
||||
v1 string
|
||||
v2 string
|
||||
expected int
|
||||
}{
|
||||
{"0.1.0", "0.1.0", 0},
|
||||
{"0.1.0", "0.1.1", -1},
|
||||
{"0.1.1", "0.1.0", 1},
|
||||
{"0.2.0", "0.1.9", 1},
|
||||
{"1.0.0", "0.9.9", 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := base.versionCompare(tt.v1, tt.v2)
|
||||
if result != tt.expected {
|
||||
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseDistribution_versionCompare_WithPrefix(t *testing.T) {
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
|
||||
tests := []struct {
|
||||
v1 string
|
||||
v2 string
|
||||
expected int
|
||||
}{
|
||||
{"v0.1.0", "v0.1.0", 0},
|
||||
{"v0.1.0", "v0.1.1", -1},
|
||||
{"v0.1.1", "v0.1.0", 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := base.versionCompare(tt.v1, tt.v2)
|
||||
if result != tt.expected {
|
||||
t.Errorf("versionCompare(%q, %q) = %d; want %d", tt.v1, tt.v2, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
533
internal/distros/debian.go
Normal file
533
internal/distros/debian.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("debian", "#A80030", FamilyDebian, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewDebianDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type DebianDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewDebianDistribution(config DistroConfig, logChan chan<- string) *DebianDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &DebianDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetID() string {
|
||||
return d.config.ID
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetColorHex() string {
|
||||
return d.config.ColorHex
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetFamily() DistroFamily {
|
||||
return d.config.Family
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerAPT
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return d.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
dependencies = append(dependencies, d.detectDMS())
|
||||
|
||||
dependencies = append(dependencies, d.detectSpecificTerminal(terminal))
|
||||
|
||||
dependencies = append(dependencies, d.detectGit())
|
||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, d.detectQuickshell())
|
||||
dependencies = append(dependencies, d.detectXDGPortal())
|
||||
dependencies = append(dependencies, d.detectPolkitAgent())
|
||||
dependencies = append(dependencies, d.detectAccountsService())
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, d.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, d.detectMatugen())
|
||||
dependencies = append(dependencies, d.detectDgop())
|
||||
dependencies = append(dependencies, d.detectHyprpicker())
|
||||
dependencies = append(dependencies, d.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if d.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
|
||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeManual, BuildFunc: "installHyprpicker"},
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Updating APT package lists",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: "Installing build-essential...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y build-essential",
|
||||
LogOutput: "Installing build tools",
|
||||
}
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
cmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev")
|
||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.12,
|
||||
Step: "Prerequisites installation complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "Prerequisites successfully installed",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := d.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, manualPkgs := d.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := d.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install APT packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.80,
|
||||
Step: "Installing build dependencies...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Installing build tools for manual compilation",
|
||||
}
|
||||
if err := d.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install build dependencies: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := d.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string) {
|
||||
systemPkgs := []string{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
packageMap := d.GetPackageMapping(wm)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
d.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
buildDeps := make(map[string]bool)
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri":
|
||||
buildDeps["curl"] = true
|
||||
buildDeps["libxkbcommon-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["libudev-dev"] = true
|
||||
buildDeps["libinput-dev"] = true
|
||||
buildDeps["libdisplay-info-dev"] = true
|
||||
buildDeps["libpango1.0-dev"] = true
|
||||
buildDeps["libcairo-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libc6-dev"] = true
|
||||
buildDeps["clang"] = true
|
||||
buildDeps["libseat-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["alacritty"] = true
|
||||
buildDeps["fuzzel"] = true
|
||||
case "quickshell":
|
||||
buildDeps["qt6-base-dev"] = true
|
||||
buildDeps["qt6-base-private-dev"] = true
|
||||
buildDeps["qt6-declarative-dev"] = true
|
||||
buildDeps["qt6-declarative-private-dev"] = true
|
||||
buildDeps["qt6-wayland-dev"] = true
|
||||
buildDeps["qt6-wayland-private-dev"] = true
|
||||
buildDeps["qt6-tools-dev"] = true
|
||||
buildDeps["libqt6svg6-dev"] = true
|
||||
buildDeps["qt6-shadertools-dev"] = true
|
||||
buildDeps["spirv-tools"] = true
|
||||
buildDeps["libcli11-dev"] = true
|
||||
buildDeps["libjemalloc-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["wayland-protocols"] = true
|
||||
buildDeps["libdrm-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["libegl-dev"] = true
|
||||
buildDeps["libgles2-mesa-dev"] = true
|
||||
buildDeps["libgl1-mesa-dev"] = true
|
||||
buildDeps["libxcb1-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libpam0g-dev"] = true
|
||||
case "ghostty":
|
||||
buildDeps["curl"] = true
|
||||
case "matugen":
|
||||
buildDeps["curl"] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri", "matugen":
|
||||
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
case "cliphist", "dgop":
|
||||
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Go: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(buildDeps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
depList := make([]string, 0, len(buildDeps))
|
||||
for dep := range buildDeps {
|
||||
depList = append(depList, dep)
|
||||
}
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, depList...)
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if d.commandExists("cargo") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.82,
|
||||
Step: "Installing rustup...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.83,
|
||||
Step: "Installing stable Rust toolchain...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "rustup install stable",
|
||||
}
|
||||
|
||||
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||
if err := d.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||
}
|
||||
|
||||
if !d.commandExists("cargo") {
|
||||
d.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if d.commandExists("go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.87,
|
||||
Step: "Installing Go...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install golang-go",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) installGhosttyDebian(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
d.log("Installing Ghostty using Debian installer script...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Running Ghostty Debian installer...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
||||
LogOutput: "Installing Ghostty using pre-built Debian package",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
||||
|
||||
if err := d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
}
|
||||
|
||||
d.log("Ghostty installed successfully using Debian installer")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
switch pkg {
|
||||
case "ghostty":
|
||||
if err := d.installGhosttyDebian(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||
}
|
||||
default:
|
||||
if err := d.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
19
internal/distros/factory.go
Normal file
19
internal/distros/factory.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
// NewDependencyDetector creates a DependencyDetector for the specified distribution
|
||||
func NewDependencyDetector(distribution string, logChan chan<- string) (deps.DependencyDetector, error) {
|
||||
distro, err := NewDistribution(distribution, logChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return distro, nil
|
||||
}
|
||||
|
||||
// NewPackageInstaller creates a Distribution for package installation
|
||||
func NewPackageInstaller(distribution string, logChan chan<- string) (Distribution, error) {
|
||||
return NewDistribution(distribution, logChan)
|
||||
}
|
||||
550
internal/distros/fedora.go
Normal file
550
internal/distros/fedora.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
|
||||
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewFedoraDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type FedoraDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewFedoraDistribution(config DistroConfig, logChan chan<- string) *FedoraDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &FedoraDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetID() string {
|
||||
return f.config.ID
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetColorHex() string {
|
||||
return f.config.ColorHex
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetFamily() DistroFamily {
|
||||
return f.config.Family
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerDNF
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return f.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, f.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, f.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, f.detectGit())
|
||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, f.detectQuickshell())
|
||||
dependencies = append(dependencies, f.detectXDGPortal())
|
||||
dependencies = append(dependencies, f.detectPolkitAgent())
|
||||
dependencies = append(dependencies, f.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, f.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, f.detectMatugen())
|
||||
dependencies = append(dependencies, f.detectDgop())
|
||||
dependencies = append(dependencies, f.detectHyprpicker())
|
||||
dependencies = append(dependencies, f.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("rpm", "-q", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return f.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard DNF packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": f.getHyprpickerMapping(variants["hyprland"]),
|
||||
|
||||
// COPR packages
|
||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = f.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = f.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = f.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms-git"}
|
||||
}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||
}
|
||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getHyprpickerMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "hyprpicker-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
||||
}
|
||||
return PackageMapping{Name: "hyprpicker", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||
}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if f.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"dnf-plugins-core",
|
||||
"make",
|
||||
"unzip",
|
||||
"libwayland-server",
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
prerequisites := f.getPrerequisites()
|
||||
var missingPkgs []string
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking prerequisite packages",
|
||||
}
|
||||
|
||||
for _, pkg := range prerequisites {
|
||||
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
missingPkgs = append(missingPkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
f.log("go not found in PATH, will install golang-bin")
|
||||
missingPkgs = append(missingPkgs, "golang-bin")
|
||||
} else {
|
||||
f.log("go already available in PATH")
|
||||
}
|
||||
|
||||
if len(missingPkgs) == 0 {
|
||||
f.log("All prerequisites already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo dnf install -y %s", strings.Join(missingPkgs, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError("failed to install prerequisites", err)
|
||||
f.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
f.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := f.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
dnfPkgs, coprPkgs, manualPkgs := f.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: Enable COPR repositories
|
||||
if len(coprPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.15,
|
||||
Step: "Enabling COPR repositories...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Setting up COPR repositories for additional packages",
|
||||
}
|
||||
if err := f.enableCOPRRepos(ctx, coprPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable COPR repositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: System Packages (DNF)
|
||||
if len(dnfPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(dnfPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(dnfPkgs, ", ")),
|
||||
}
|
||||
if err := f.installDNFPackages(ctx, dnfPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install DNF packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: COPR Packages
|
||||
coprPkgNames := f.extractPackageNames(coprPkgs)
|
||||
if len(coprPkgNames) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages, // Reusing AUR phase for COPR
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d COPR packages...", len(coprPkgNames)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing COPR packages: %s", strings.Join(coprPkgNames, ", ")),
|
||||
}
|
||||
if err := f.installCOPRPackages(ctx, coprPkgNames, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install COPR packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := f.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 7: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []PackageMapping, []string) {
|
||||
dnfPkgs := []string{}
|
||||
coprPkgs := []PackageMapping{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := f.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
f.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
dnfPkgs = append(dnfPkgs, pkgInfo.Name)
|
||||
case RepoTypeCOPR:
|
||||
coprPkgs = append(coprPkgs, pkgInfo)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return dnfPkgs, coprPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
for _, pkg := range coprPkgs {
|
||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||
f.log(fmt.Sprintf("Enabling COPR repository: %s", pkg.RepoURL))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.20,
|
||||
Step: fmt.Sprintf("Enabling COPR repo %s...", pkg.RepoURL),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError(fmt.Sprintf("failed to enable COPR repo %s", pkg.RepoURL), err)
|
||||
f.log(fmt.Sprintf("COPR enable command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to enable COPR repo %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
f.log(fmt.Sprintf("COPR repo %s enabled successfully: %s", pkg.RepoURL, string(output)))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
|
||||
// Special handling for niri COPR repo - set priority=1
|
||||
if pkg.RepoURL == "yalter/niri-git" {
|
||||
f.log("Setting priority=1 for niri-git COPR repo...")
|
||||
repoFile := "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo"
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.22,
|
||||
Step: "Setting niri COPR repo priority...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||
}
|
||||
|
||||
priorityCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s' 2>&1", repoFile))
|
||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
f.logError("failed to set niri COPR repo priority", err)
|
||||
f.log(fmt.Sprintf("Priority command output: %s", string(priorityOutput)))
|
||||
return fmt.Errorf("failed to set niri COPR repo priority: %w", err)
|
||||
}
|
||||
f.log(fmt.Sprintf("niri COPR repo priority set successfully: %s", string(priorityOutput)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"dnf", "install", "-y"}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "niri" || pkg == "niri-git" {
|
||||
args = append(args, "--setopt=install_weak_deps=False")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing COPR packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
}
|
||||
734
internal/distros/gentoo.go
Normal file
734
internal/distros/gentoo.go
Normal file
@@ -0,0 +1,734 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
var GentooGlobalUseFlags = []string{
|
||||
"X",
|
||||
"dbus",
|
||||
"udev",
|
||||
"alsa",
|
||||
"policykit",
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
"gif",
|
||||
"tiff",
|
||||
"svg",
|
||||
"brotli",
|
||||
"gdbm",
|
||||
"accessibility",
|
||||
"gtk",
|
||||
"qt6",
|
||||
"egl",
|
||||
"gbm",
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register("gentoo", "#54487A", FamilyGentoo, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewGentooDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type GentooDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &GentooDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getArchKeyword() string {
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
case "amd64":
|
||||
return "~amd64"
|
||||
case "arm64":
|
||||
return "~arm64"
|
||||
default:
|
||||
return "~amd64"
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetID() string {
|
||||
return g.config.ID
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetColorHex() string {
|
||||
return g.config.ColorHex
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetFamily() DistroFamily {
|
||||
return g.config.Family
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerPortage
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return g.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
dependencies = append(dependencies, g.detectDMS())
|
||||
|
||||
dependencies = append(dependencies, g.detectSpecificTerminal(terminal))
|
||||
|
||||
dependencies = append(dependencies, g.detectGit())
|
||||
dependencies = append(dependencies, g.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, g.detectQuickshell())
|
||||
dependencies = append(dependencies, g.detectXDGPortal())
|
||||
dependencies = append(dependencies, g.detectPolkitAgent())
|
||||
dependencies = append(dependencies, g.detectAccountsService())
|
||||
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, g.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, g.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, g.detectMatugen())
|
||||
dependencies = append(dependencies, g.detectDgop())
|
||||
dependencies = append(dependencies, g.detectHyprpicker())
|
||||
dependencies = append(dependencies, g.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("mate-extra/mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("gui-apps/xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if g.packageInstalled("sys-apps/accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("qlist", "-I", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return g.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
archKeyword := g.getArchKeyword()
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": g.getHyprpickerMapping(variants["hyprland"]),
|
||||
|
||||
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||
"qtdeclarative": {Name: "dev-qt/qtdeclarative", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||
"qtwayland": {Name: "dev-qt/qtwayland", Repository: RepoTypeSystem},
|
||||
"mesa": {Name: "media-libs/mesa", Repository: RepoTypeSystem, UseFlags: "opengl vulkan"},
|
||||
|
||||
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = g.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grim"] = PackageMapping{Name: "gui-apps/grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "gui-apps/slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = g.getHyprlandMapping(variants["hyprland"])
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "app-misc/jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = g.getNiriMapping(variants["niri"])
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "gui-apps/xwayland-satellite", Repository: RepoTypeGURU, AcceptKeywords: archKeyword}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-apps/quickshell", Repository: RepoTypeGURU, UseFlags: "breakpad jemalloc sockets wayland layer-shell session-lock toplevel-management screencopy X pipewire tray mpris pam hyprland hyprland-global-shortcuts hyprland-focus-grab i3 i3-ipc bluetooth", AcceptKeywords: "**"}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
||||
archKeyword := g.getArchKeyword()
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||
}
|
||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getHyprpickerMapping(_ deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-apps/hyprpicker", Repository: RepoTypeGURU, AcceptKeywords: g.getArchKeyword()}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
return PackageMapping{Name: "gui-wm/niri", Repository: RepoTypeGURU, UseFlags: "dbus screencast", AcceptKeywords: g.getArchKeyword()}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"app-eselect/eselect-repository",
|
||||
"dev-vcs/git",
|
||||
"dev-build/make",
|
||||
"app-arch/unzip",
|
||||
"dev-util/pkgconf",
|
||||
"dev-qt/qtdeclarative",
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword string) error {
|
||||
useFlags := strings.Join(GentooGlobalUseFlags, " ")
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "grep", "-q", "^USE=", "/etc/portage/make.conf")
|
||||
hasUse := checkCmd.Run() == nil
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if hasUse {
|
||||
cmd = execSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf; exit_code=$?; exit $exit_code", useFlags))
|
||||
} else {
|
||||
cmd = execSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"; exit_code=$?; exit $exit_code", useFlags))
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.log(fmt.Sprintf("Failed to set global USE flags: %s", string(output)))
|
||||
return err
|
||||
}
|
||||
|
||||
g.log(fmt.Sprintf("Set global USE flags: %s", useFlags))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
prerequisites := g.getPrerequisites()
|
||||
var missingPkgs []string
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Setting global USE flags...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Configuring global USE flags in /etc/portage/make.conf",
|
||||
}
|
||||
|
||||
if err := g.setGlobalUseFlags(ctx, sudoPassword); err != nil {
|
||||
g.logError("failed to set global USE flags", err)
|
||||
return fmt.Errorf("failed to set global USE flags: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking prerequisite packages",
|
||||
}
|
||||
|
||||
for _, pkg := range prerequisites {
|
||||
checkCmd := exec.CommandContext(ctx, "qlist", "-I", pkg)
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
missingPkgs = append(missingPkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
g.log("go not found in PATH, will install dev-lang/go")
|
||||
missingPkgs = append(missingPkgs, "dev-lang/go")
|
||||
} else {
|
||||
g.log("go already available in PATH")
|
||||
}
|
||||
|
||||
if len(missingPkgs) == 0 {
|
||||
g.log("All prerequisites already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.07,
|
||||
Step: "Syncing Portage tree...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo emerge --sync",
|
||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||
}
|
||||
|
||||
syncCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"emerge --sync --quiet; exit_code=$?; exit $exit_code")
|
||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||
if syncErr != nil {
|
||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||
return fmt.Errorf("failed to sync Portage tree: %w\nOutput: %s", syncErr, string(syncOutput))
|
||||
}
|
||||
g.log("Portage tree synced successfully")
|
||||
|
||||
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := execSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s; exit_code=$?; exit $exit_code", strings.Join(args, " ")))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.logError("failed to install prerequisites", err)
|
||||
g.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to install prerequisites: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
g.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := g.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, guruPkgs, manualPkgs := g.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
g.log(fmt.Sprintf("CATEGORIZED PACKAGES: system=%d, guru=%d, manual=%d", len(systemPkgs), len(guruPkgs), len(manualPkgs)))
|
||||
|
||||
if len(systemPkgs) > 0 {
|
||||
systemPkgNames := g.extractPackageNames(systemPkgs)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgNames, ", ")),
|
||||
}
|
||||
if err := g.installPortagePackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Portage packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(guruPkgs) > 0 {
|
||||
g.log(fmt.Sprintf("FOUND %d GURU PACKAGES - WILL SYNC GURU REPO", len(guruPkgs)))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.60,
|
||||
Step: "Syncing GURU repository...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Syncing GURU repository to fetch latest ebuilds",
|
||||
}
|
||||
g.log("ABOUT TO CALL syncGURURepo")
|
||||
if err := g.syncGURURepo(ctx, sudoPassword, progressChan); err != nil {
|
||||
g.log(fmt.Sprintf("syncGURURepo RETURNED ERROR: %v", err))
|
||||
return fmt.Errorf("failed to sync GURU repository: %w", err)
|
||||
}
|
||||
g.log("syncGURURepo COMPLETED SUCCESSFULLY")
|
||||
|
||||
guruPkgNames := g.extractPackageNames(guruPkgs)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d GURU packages...", len(guruPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing GURU packages: %s", strings.Join(guruPkgNames, ", ")),
|
||||
}
|
||||
if err := g.installGURUPackages(ctx, guruPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install GURU packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := g.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]PackageMapping, []PackageMapping, []string) {
|
||||
systemPkgs := []PackageMapping{}
|
||||
guruPkgs := []PackageMapping{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := g.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
g.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo)
|
||||
case RepoTypeGURU:
|
||||
guruPkgs = append(guruPkgs, pkgInfo)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, guruPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) installPortagePackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
packageNames := g.extractPackageNames(packages)
|
||||
g.log(fmt.Sprintf("Installing Portage packages: %s", strings.Join(packageNames, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg.AcceptKeywords != "" {
|
||||
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
if pkg.UseFlags != "" {
|
||||
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, packageNames...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s || exit $?", strings.Join(args, " ")))
|
||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||
packageUseDir := "/etc/portage/package.use"
|
||||
|
||||
mkdirCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||
return fmt.Errorf("failed to create package.use directory: %w", err)
|
||||
}
|
||||
|
||||
useFlagLine := fmt.Sprintf("%s %s", packageName, useFlags)
|
||||
|
||||
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, packageUseDir))
|
||||
if checkExistingCmd.Run() == nil {
|
||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||
replaceCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||
return fmt.Errorf("failed to remove old USE flags: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
appendCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||
|
||||
output, err := appendCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.log(fmt.Sprintf("append output: %s", string(output)))
|
||||
return fmt.Errorf("failed to write USE flags to package.use: %w", err)
|
||||
}
|
||||
|
||||
g.log(fmt.Sprintf("Set USE flags for %s: %s", packageName, useFlags))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.55,
|
||||
Step: "Enabling GURU repository...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo eselect repository enable guru",
|
||||
LogOutput: "Enabling GURU repository with eselect",
|
||||
}
|
||||
|
||||
// Enable GURU repository
|
||||
enableCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||
output, err := enableCmd.CombinedOutput()
|
||||
|
||||
g.log(fmt.Sprintf("eselect repository enable guru output:\n%s", string(output)))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.55,
|
||||
LogOutput: "GURU repository enabled",
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.55,
|
||||
LogOutput: fmt.Sprintf("ERROR enabling GURU: %v", err),
|
||||
Error: err,
|
||||
}
|
||||
return fmt.Errorf("failed to enable GURU repository: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
|
||||
// Sync GURU repository
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.57,
|
||||
Step: "Syncing GURU repository...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo emaint sync --repo guru",
|
||||
LogOutput: "Syncing GURU repository",
|
||||
}
|
||||
|
||||
syncCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||
|
||||
g.log(fmt.Sprintf("emaint sync --repo guru output:\n%s", string(syncOutput)))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.57,
|
||||
LogOutput: "GURU repository synced",
|
||||
}
|
||||
|
||||
if syncErr != nil {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.57,
|
||||
LogOutput: fmt.Sprintf("ERROR syncing GURU: %v", syncErr),
|
||||
Error: syncErr,
|
||||
}
|
||||
return fmt.Errorf("failed to sync GURU repository: %w\nOutput: %s", syncErr, string(syncOutput))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packageName, keywords, sudoPassword string) error {
|
||||
checkCmd := exec.CommandContext(ctx, "portageq", "match", "/", packageName)
|
||||
if output, err := checkCmd.CombinedOutput(); err == nil && len(output) > 0 {
|
||||
g.log(fmt.Sprintf("Package %s is already available (may already be unmasked)", packageName))
|
||||
return nil
|
||||
}
|
||||
|
||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||
|
||||
mkdirCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||
return fmt.Errorf("failed to create package.accept_keywords directory: %w", err)
|
||||
}
|
||||
|
||||
keywordLine := fmt.Sprintf("%s %s", packageName, keywords)
|
||||
|
||||
checkExistingCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||
fmt.Sprintf("grep -q '^%s ' %s/danklinux 2>/dev/null", packageName, acceptKeywordsDir))
|
||||
if checkExistingCmd.Run() == nil {
|
||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||
replaceCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||
return fmt.Errorf("failed to remove old accept keywords: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
appendCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||
|
||||
output, err := appendCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
g.log(fmt.Sprintf("append output: %s", string(output)))
|
||||
return fmt.Errorf("failed to write accept keywords: %w", err)
|
||||
}
|
||||
|
||||
g.log(fmt.Sprintf("Set accept keywords for %s: %s", packageName, keywords))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
packageNames := g.extractPackageNames(packages)
|
||||
g.log(fmt.Sprintf("Installing GURU packages: %s", strings.Join(packageNames, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg.AcceptKeywords != "" {
|
||||
if err := g.setPackageAcceptKeywords(ctx, pkg.Name, pkg.AcceptKeywords, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set accept keywords for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
if pkg.UseFlags != "" {
|
||||
if err := g.setPackageUseFlags(ctx, pkg.Name, pkg.UseFlags, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to set USE flags for %s: %w", pkg.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guruPackages := make([]string, len(packageNames))
|
||||
for i, pkg := range packageNames {
|
||||
guruPackages[i] = pkg + "::guru"
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, guruPackages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing GURU packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, fmt.Sprintf("%s || exit $?", strings.Join(args, " ")))
|
||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||
}
|
||||
156
internal/distros/interface.go
Normal file
156
internal/distros/interface.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
// DistroFamily represents a family of related distributions
|
||||
type DistroFamily string
|
||||
|
||||
const (
|
||||
FamilyArch DistroFamily = "arch"
|
||||
FamilyFedora DistroFamily = "fedora"
|
||||
FamilySUSE DistroFamily = "suse"
|
||||
FamilyUbuntu DistroFamily = "ubuntu"
|
||||
FamilyDebian DistroFamily = "debian"
|
||||
FamilyNix DistroFamily = "nix"
|
||||
FamilyGentoo DistroFamily = "gentoo"
|
||||
)
|
||||
|
||||
// PackageManagerType defines the package manager a distro uses
|
||||
type PackageManagerType string
|
||||
|
||||
const (
|
||||
PackageManagerPacman PackageManagerType = "pacman"
|
||||
PackageManagerDNF PackageManagerType = "dnf"
|
||||
PackageManagerAPT PackageManagerType = "apt"
|
||||
PackageManagerZypper PackageManagerType = "zypper"
|
||||
PackageManagerNix PackageManagerType = "nix"
|
||||
PackageManagerPortage PackageManagerType = "portage"
|
||||
)
|
||||
|
||||
// RepositoryType defines the type of repository for a package
|
||||
type RepositoryType string
|
||||
|
||||
const (
|
||||
RepoTypeSystem RepositoryType = "system" // Standard system repo (pacman, dnf, apt)
|
||||
RepoTypeAUR RepositoryType = "aur" // Arch User Repository
|
||||
RepoTypeCOPR RepositoryType = "copr" // Fedora COPR
|
||||
RepoTypePPA RepositoryType = "ppa" // Ubuntu PPA
|
||||
RepoTypeFlake RepositoryType = "flake" // Nix flake
|
||||
RepoTypeGURU RepositoryType = "guru" // Gentoo GURU
|
||||
RepoTypeManual RepositoryType = "manual" // Manual build from source
|
||||
)
|
||||
|
||||
// InstallPhase represents the current phase of installation
|
||||
type InstallPhase int
|
||||
|
||||
const (
|
||||
PhasePrerequisites InstallPhase = iota
|
||||
PhaseAURHelper
|
||||
PhaseSystemPackages
|
||||
PhaseAURPackages
|
||||
PhaseCursorTheme
|
||||
PhaseConfiguration
|
||||
PhaseComplete
|
||||
)
|
||||
|
||||
// InstallProgressMsg represents progress during package installation
|
||||
type InstallProgressMsg struct {
|
||||
Phase InstallPhase
|
||||
Progress float64
|
||||
Step string
|
||||
IsComplete bool
|
||||
NeedsSudo bool
|
||||
CommandInfo string
|
||||
LogOutput string
|
||||
Error error
|
||||
}
|
||||
|
||||
// PackageMapping defines how to install a package on a specific distro
|
||||
type PackageMapping struct {
|
||||
Name string // Package name to install
|
||||
Repository RepositoryType // Repository type
|
||||
RepoURL string // Repository URL if needed (e.g., COPR repo, PPA)
|
||||
BuildFunc string // Name of manual build function if RepoTypeManual
|
||||
UseFlags string // USE flags for Gentoo packages
|
||||
AcceptKeywords string // Accept keywords for Gentoo packages (e.g., "~amd64")
|
||||
}
|
||||
|
||||
// Distribution defines a Linux distribution with all its specific configurations
|
||||
type Distribution interface {
|
||||
// Metadata
|
||||
GetID() string
|
||||
GetColorHex() string
|
||||
GetFamily() DistroFamily
|
||||
GetPackageManager() PackageManagerType
|
||||
|
||||
// Dependency Detection
|
||||
DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error)
|
||||
DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error)
|
||||
|
||||
// Package Installation
|
||||
InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error
|
||||
|
||||
// Package Mapping
|
||||
GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping
|
||||
|
||||
// Prerequisites
|
||||
InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error
|
||||
}
|
||||
|
||||
// DistroConfig holds configuration for a distribution
|
||||
type DistroConfig struct {
|
||||
ID string
|
||||
ColorHex string
|
||||
Family DistroFamily
|
||||
Constructor func(config DistroConfig, logChan chan<- string) Distribution
|
||||
}
|
||||
|
||||
// Registry holds all supported distributions
|
||||
var Registry = make(map[string]DistroConfig)
|
||||
|
||||
// Register adds a distribution to the registry
|
||||
func Register(id, colorHex string, family DistroFamily, constructor func(config DistroConfig, logChan chan<- string) Distribution) {
|
||||
Registry[id] = DistroConfig{
|
||||
ID: id,
|
||||
ColorHex: colorHex,
|
||||
Family: family,
|
||||
Constructor: constructor,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSupportedDistros returns a list of all supported distribution IDs
|
||||
func GetSupportedDistros() []string {
|
||||
ids := make([]string, 0, len(Registry))
|
||||
for id := range Registry {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// IsDistroSupported checks if a distribution ID is supported
|
||||
func IsDistroSupported(id string) bool {
|
||||
_, exists := Registry[id]
|
||||
return exists
|
||||
}
|
||||
|
||||
// NewDistribution creates a distribution instance by ID
|
||||
func NewDistribution(id string, logChan chan<- string) (Distribution, error) {
|
||||
config, exists := Registry[id]
|
||||
if !exists {
|
||||
return nil, &UnsupportedDistributionError{ID: id}
|
||||
}
|
||||
return config.Constructor(config, logChan), nil
|
||||
}
|
||||
|
||||
// UnsupportedDistributionError is returned when a distribution is not supported
|
||||
type UnsupportedDistributionError struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
func (e *UnsupportedDistributionError) Error() string {
|
||||
return "unsupported distribution: " + e.ID
|
||||
}
|
||||
802
internal/distros/manual_packages.go
Normal file
802
internal/distros/manual_packages.go
Normal file
@@ -0,0 +1,802 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ManualPackageInstaller provides methods for installing packages from source
|
||||
type ManualPackageInstaller struct {
|
||||
*BaseDistribution
|
||||
}
|
||||
|
||||
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
||||
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
||||
parts := strings.Split(line, "refs/tags/")
|
||||
if len(parts) > 1 {
|
||||
latestTag := strings.TrimSpace(parts[1])
|
||||
return latestTag
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getLatestQuickshellTag fetches the latest tag from the quickshell repository
|
||||
func (m *ManualPackageInstaller) getLatestQuickshellTag(ctx context.Context) string {
|
||||
tagCmd := exec.CommandContext(ctx, "git", "ls-remote", "--tags", "--sort=-v:refname",
|
||||
"https://github.com/quickshell-mirror/quickshell.git")
|
||||
tagOutput, err := tagCmd.Output()
|
||||
if err != nil {
|
||||
m.log(fmt.Sprintf("Warning: failed to fetch quickshell tags: %v", err))
|
||||
return ""
|
||||
}
|
||||
|
||||
return m.parseLatestTagFromGitOutput(string(tagOutput))
|
||||
}
|
||||
|
||||
// InstallManualPackages handles packages that need manual building
|
||||
func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
switch pkg {
|
||||
case "dms (DankMaterialShell)", "dms":
|
||||
if err := m.installDankMaterialShell(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install DankMaterialShell: %w", err)
|
||||
}
|
||||
case "dgop":
|
||||
if err := m.installDgop(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install dgop: %w", err)
|
||||
}
|
||||
case "grimblast":
|
||||
if err := m.installGrimblast(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
||||
}
|
||||
case "niri":
|
||||
if err := m.installNiri(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install niri: %w", err)
|
||||
}
|
||||
case "quickshell":
|
||||
if err := m.installQuickshell(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
case "hyprland":
|
||||
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install hyprland: %w", err)
|
||||
}
|
||||
case "hyprpicker":
|
||||
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||
}
|
||||
case "ghostty":
|
||||
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||
}
|
||||
case "matugen":
|
||||
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install matugen: %w", err)
|
||||
}
|
||||
case "cliphist":
|
||||
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||
}
|
||||
case "xwayland-satellite":
|
||||
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||
}
|
||||
default:
|
||||
m.log(fmt.Sprintf("Warning: No manual build method for %s", pkg))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing dgop from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning dgop repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/AvengeMedia/dgop.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/AvengeMedia/dgop.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
m.logError("failed to clone dgop repository", err)
|
||||
return fmt.Errorf("failed to clone dgop repository: %w", err)
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "make")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.7, "Building dgop..."); err != nil {
|
||||
return fmt.Errorf("failed to build dgop: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing dgop...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, "make install")
|
||||
installCmd.Dir = tmpDir
|
||||
if err := installCmd.Run(); err != nil {
|
||||
m.logError("failed to install dgop", err)
|
||||
return fmt.Errorf("failed to install dgop: %w", err)
|
||||
}
|
||||
|
||||
m.log("dgop installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installGrimblast(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing grimblast script for Hyprland...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Downloading grimblast script...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "curl grimblast script",
|
||||
}
|
||||
|
||||
grimblastURL := "https://raw.githubusercontent.com/hyprwm/contrib/refs/heads/main/grimblast/grimblast"
|
||||
tmpPath := filepath.Join(os.TempDir(), "grimblast")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", "-o", tmpPath, grimblastURL)
|
||||
if err := downloadCmd.Run(); err != nil {
|
||||
m.logError("failed to download grimblast", err)
|
||||
return fmt.Errorf("failed to download grimblast: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.5,
|
||||
Step: "Making grimblast executable...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "chmod +x grimblast",
|
||||
}
|
||||
|
||||
chmodCmd := exec.CommandContext(ctx, "chmod", "+x", tmpPath)
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
m.logError("failed to make grimblast executable", err)
|
||||
return fmt.Errorf("failed to make grimblast executable: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing grimblast to /usr/local/bin...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp grimblast /usr/local/bin/",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s /usr/local/bin/grimblast", tmpPath))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
m.logError("failed to install grimblast", err)
|
||||
return fmt.Errorf("failed to install grimblast: %w", err)
|
||||
}
|
||||
|
||||
os.Remove(tmpPath)
|
||||
|
||||
m.log("grimblast installed successfully to /usr/local/bin")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing niri from source...")
|
||||
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
os.RemoveAll(buildDir)
|
||||
os.RemoveAll(tmpDir)
|
||||
}()
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.2,
|
||||
Step: "Cloning niri repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/YaLTeR/niri.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/YaLTeR/niri.git", buildDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone niri: %w", err)
|
||||
}
|
||||
|
||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", buildDir, "checkout", "v25.08")
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
m.log(fmt.Sprintf("Warning: failed to checkout v25.08, using main: %v", err))
|
||||
}
|
||||
|
||||
if !m.commandExists("cargo-deb") {
|
||||
cargoDebInstallCmd := exec.CommandContext(ctx, "cargo", "install", "cargo-deb")
|
||||
cargoDebInstallCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
|
||||
if err := m.runWithProgressStep(cargoDebInstallCmd, progressChan, PhaseSystemPackages, 0.3, 0.35, "Installing cargo-deb..."); err != nil {
|
||||
return fmt.Errorf("failed to install cargo-deb: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
buildDebCmd := exec.CommandContext(ctx, "cargo", "deb")
|
||||
buildDebCmd.Dir = buildDir
|
||||
buildDebCmd.Env = append(os.Environ(), "TMPDIR="+tmpDir)
|
||||
if err := m.runWithProgressStep(buildDebCmd, progressChan, PhaseSystemPackages, 0.35, 0.95, "Building niri deb package..."); err != nil {
|
||||
return fmt.Errorf("failed to build niri deb: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.95,
|
||||
Step: "Installing niri deb package...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "dpkg -i niri.deb",
|
||||
}
|
||||
|
||||
installDebCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||
|
||||
output, err := installDebCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.log(fmt.Sprintf("dpkg install failed. Output:\n%s", string(output)))
|
||||
return fmt.Errorf("failed to install niri deb package: %w\nOutput:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("dpkg install successful. Output:\n%s", string(output)))
|
||||
|
||||
m.log("niri installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing quickshell from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning quickshell repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
|
||||
}
|
||||
|
||||
var cloneCmd *exec.Cmd
|
||||
if forceQuickshellGit {
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
// Get latest tag from repository
|
||||
latestTag := m.getLatestQuickshellTag(ctx)
|
||||
if latestTag != "" {
|
||||
m.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
m.log("Warning: failed to fetch latest tag, using default branch")
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
}
|
||||
}
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone quickshell: %w", err)
|
||||
}
|
||||
|
||||
buildDir := tmpDir + "/build"
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.3,
|
||||
Step: "Configuring quickshell build...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake -B build -S . -G Ninja",
|
||||
}
|
||||
|
||||
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
|
||||
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||
"-DCRASH_REPORTER=off",
|
||||
"-DCMAKE_CXX_STANDARD=20")
|
||||
configureCmd.Dir = tmpDir
|
||||
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
|
||||
output, err := configureCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
m.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.4,
|
||||
Step: "Building quickshell (this may take a while)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake --build build",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
|
||||
return fmt.Errorf("failed to build quickshell: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing quickshell...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cmake --install build",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && cmake --install build", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
|
||||
m.log("quickshell installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing Hyprland from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning Hyprland repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone --recursive https://github.com/hyprwm/Hyprland.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recursive", "https://github.com/hyprwm/Hyprland.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone Hyprland: %w", err)
|
||||
}
|
||||
|
||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", tmpDir, "checkout", "v0.50.1")
|
||||
if err := checkoutCmd.Run(); err != nil {
|
||||
m.log(fmt.Sprintf("Warning: failed to checkout v0.50.1, using main: %v", err))
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "make", "all")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.2, 0.8, "Building Hyprland..."); err != nil {
|
||||
return fmt.Errorf("failed to build Hyprland: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing Hyprland...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && make install", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||
}
|
||||
|
||||
m.log("Hyprland installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing hyprpicker from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.2,
|
||||
Step: "Cloning hyprpicker repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone hyprpicker: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.4,
|
||||
Step: "Building hyprpicker...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "make all",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "make", "all")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := buildCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing hyprpicker...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo make install",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && make install", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
||||
}
|
||||
|
||||
m.log("hyprpicker installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing Ghostty from source...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning Ghostty repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/ghostty-org/ghostty.git",
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/ghostty-org/ghostty.git", tmpDir)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone Ghostty: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.2,
|
||||
Step: "Building Ghostty (this may take a while)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "zig build -Doptimize=ReleaseFast",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "zig", "build", "-Doptimize=ReleaseFast")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
||||
if err := buildCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to build Ghostty: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing Ghostty...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
}
|
||||
|
||||
m.log("Ghostty installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing matugen from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing matugen via cargo...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cargo install matugen",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "cargo", "install", "matugen")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building matugen..."); err != nil {
|
||||
return fmt.Errorf("failed to install matugen: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "matugen")
|
||||
targetPath := "/usr/local/bin/matugen"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing matugen binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("matugen installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing DankMaterialShell (DMS)...")
|
||||
|
||||
// Always install/update the DMS binary
|
||||
if err := m.installDMSBinary(ctx, sudoPassword, progressChan); err != nil {
|
||||
m.logError("Failed to install DMS binary", err)
|
||||
}
|
||||
|
||||
// Handle DMS config - clone if missing, pull if exists
|
||||
dmsPath := filepath.Join(os.Getenv("HOME"), ".config/quickshell/dms")
|
||||
if _, err := os.Stat(dmsPath); os.IsNotExist(err) {
|
||||
// Config doesn't exist, clone it
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.90,
|
||||
Step: "Cloning DankMaterialShell config...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git ~/.config/quickshell/dms",
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(dmsPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||
}
|
||||
|
||||
if !forceDMSGit {
|
||||
fetchCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "fetch", "--tags")
|
||||
if err := fetchCmd.Run(); err == nil {
|
||||
tagCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "describe", "--tags", "--abbrev=0", "origin/master")
|
||||
if tagOutput, err := tagCmd.Output(); err == nil {
|
||||
latestTag := strings.TrimSpace(string(tagOutput))
|
||||
checkoutCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "checkout", latestTag)
|
||||
if err := checkoutCmd.Run(); err == nil {
|
||||
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.log("DankMaterialShell config cloned successfully")
|
||||
} else {
|
||||
// Config exists, update it
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.90,
|
||||
Step: "Updating DankMaterialShell config...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git pull in ~/.config/quickshell/dms",
|
||||
}
|
||||
|
||||
pullCmd := exec.CommandContext(ctx, "git", "pull")
|
||||
pullCmd.Dir = dmsPath
|
||||
if err := pullCmd.Run(); err != nil {
|
||||
m.logError("Failed to update DankMaterialShell config", err)
|
||||
} else {
|
||||
m.log("DankMaterialShell config updated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing cliphist from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing cliphist via go install...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
||||
targetPath := "/usr/local/bin/cliphist"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing cliphist binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
// Make it executable
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("cliphist installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing xwayland-satellite from source...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Installing xwayland-satellite via cargo...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cargo install --git https://github.com/Supreeeme/xwayland-satellite --tag v0.7",
|
||||
}
|
||||
|
||||
installCmd := exec.CommandContext(ctx, "cargo", "install", "--git", "https://github.com/Supreeeme/xwayland-satellite", "--tag", "v0.7")
|
||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building xwayland-satellite..."); err != nil {
|
||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||
}
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
sourcePath := filepath.Join(homeDir, ".cargo", "bin", "xwayland-satellite")
|
||||
targetPath := "/usr/local/bin/xwayland-satellite"
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.7,
|
||||
Step: "Installing xwayland-satellite binary to system...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||
}
|
||||
|
||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := copyCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||
}
|
||||
|
||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
||||
if err := chmodCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||
}
|
||||
|
||||
m.log("xwayland-satellite installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
122
internal/distros/manual_packages_test.go
Normal file
122
internal/distros/manual_packages_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManualPackageInstaller_parseLatestTagFromGitOutput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "normal tag output",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
|
||||
703a3789083d2f990c4e99cd25c97c2a4cccbd81 refs/tags/v0.1.0`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
{
|
||||
name: "annotated tags with ^{}",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.2.1^{}
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
{
|
||||
name: "mixed tags",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0
|
||||
b1b150fab00a93ea983aaca5df55304bc837f51c refs/tags/v0.3.0^{}
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0
|
||||
c1c150fab00a93ea983aaca5df55304bc837f51d refs/tags/beta-1`,
|
||||
expected: "v0.3.0",
|
||||
},
|
||||
{
|
||||
name: "empty output",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "no tags",
|
||||
input: "some other output\nwithout tags",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "only annotated tags",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1^{}
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0^{}`,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single tag",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.0.0`,
|
||||
expected: "v1.0.0",
|
||||
},
|
||||
{
|
||||
name: "tag with extra whitespace",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
{
|
||||
name: "beta and rc tags",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.3.0-beta.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v0.2.0`,
|
||||
expected: "v0.3.0-beta.1",
|
||||
},
|
||||
{
|
||||
name: "tags without v prefix",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/0.2.1
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/0.2.0`,
|
||||
expected: "0.2.1",
|
||||
},
|
||||
{
|
||||
name: "multiple lines with spaces",
|
||||
input: `
|
||||
a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v1.2.3
|
||||
a5431dd02dc23d9ef1680e67777fed00fe5f7cda refs/tags/v1.2.2
|
||||
`,
|
||||
expected: "v1.2.3",
|
||||
},
|
||||
{
|
||||
name: "tag at end of line",
|
||||
input: `a1a150fab00a93ea983aaca5df55304bc837f51b refs/tags/v0.2.1`,
|
||||
expected: "v0.2.1",
|
||||
},
|
||||
}
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
installer := &ManualPackageInstaller{BaseDistribution: base}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := installer.parseLatestTagFromGitOutput(tt.input)
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseLatestTagFromGitOutput() = %q, expected %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManualPackageInstaller_parseLatestTagFromGitOutput_EmptyInstaller(t *testing.T) {
|
||||
// Test that parsing works even with a minimal installer setup
|
||||
logChan := make(chan string, 10)
|
||||
defer close(logChan)
|
||||
|
||||
base := NewBaseDistribution(logChan)
|
||||
installer := &ManualPackageInstaller{BaseDistribution: base}
|
||||
|
||||
input := `abc123 refs/tags/v1.0.0
|
||||
def456 refs/tags/v0.9.0`
|
||||
|
||||
result := installer.parseLatestTagFromGitOutput(input)
|
||||
|
||||
if result != "v1.0.0" {
|
||||
t.Errorf("Expected v1.0.0, got %s", result)
|
||||
}
|
||||
}
|
||||
458
internal/distros/nixos.go
Normal file
458
internal/distros/nixos.go
Normal file
@@ -0,0 +1,458 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("nixos", "#7EBAE4", FamilyNix, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewNixOSDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type NixOSDistribution struct {
|
||||
*BaseDistribution
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewNixOSDistribution(config DistroConfig, logChan chan<- string) *NixOSDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &NixOSDistribution{
|
||||
BaseDistribution: base,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetID() string {
|
||||
return n.config.ID
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetColorHex() string {
|
||||
return n.config.ColorHex
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetFamily() DistroFamily {
|
||||
return n.config.Family
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerNix
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return n.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, n.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, n.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, n.detectGit())
|
||||
dependencies = append(dependencies, n.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, n.detectQuickshell())
|
||||
dependencies = append(dependencies, n.detectXDGPortal())
|
||||
dependencies = append(dependencies, n.detectPolkitAgent())
|
||||
dependencies = append(dependencies, n.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, n.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, n.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, n.detectMatugen())
|
||||
dependencies = append(dependencies, n.detectDgop())
|
||||
dependencies = append(dependencies, n.detectHyprpicker())
|
||||
dependencies = append(dependencies, n.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectDMS() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
|
||||
// For NixOS, check if quickshell can find the dms config
|
||||
cmd := exec.Command("qs", "-c", "dms", "--list")
|
||||
if err := cmd.Run(); err == nil {
|
||||
status = deps.StatusInstalled
|
||||
} else if n.packageInstalled("DankMaterialShell") {
|
||||
// Fallback: check if flake is in profile
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "dms (DankMaterialShell)",
|
||||
Status: status,
|
||||
Description: "Desktop Management System configuration (installed as flake)",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectWindowManager(wm deps.WindowManager) deps.Dependency {
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
status := deps.StatusMissing
|
||||
description := "Dynamic tiling Wayland compositor"
|
||||
if n.commandExists("hyprland") || n.commandExists("Hyprland") {
|
||||
status = deps.StatusInstalled
|
||||
} else {
|
||||
description = "Install system-wide: programs.hyprland.enable = true; in configuration.nix"
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "hyprland",
|
||||
Status: status,
|
||||
Description: description,
|
||||
Required: true,
|
||||
}
|
||||
case deps.WindowManagerNiri:
|
||||
status := deps.StatusMissing
|
||||
description := "Scrollable-tiling Wayland compositor"
|
||||
if n.commandExists("niri") {
|
||||
status = deps.StatusInstalled
|
||||
} else {
|
||||
description = "Install system-wide: environment.systemPackages = [ pkgs.niri ]; in configuration.nix"
|
||||
}
|
||||
return deps.Dependency{
|
||||
Name: "niri",
|
||||
Status: status,
|
||||
Description: description,
|
||||
Required: true,
|
||||
}
|
||||
default:
|
||||
return deps.Dependency{
|
||||
Name: "unknown-wm",
|
||||
Status: deps.StatusMissing,
|
||||
Description: "Unknown window manager",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectHyprlandTools() []deps.Dependency {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
tools := []struct {
|
||||
name string
|
||||
description string
|
||||
}{
|
||||
{"grim", "Screenshot utility for Wayland"},
|
||||
{"slurp", "Region selection utility for Wayland"},
|
||||
{"hyprctl", "Hyprland control utility (comes with system Hyprland)"},
|
||||
{"hyprpicker", "Color picker for Hyprland"},
|
||||
{"grimblast", "Screenshot script for Hyprland"},
|
||||
{"jq", "JSON processor"},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
status := deps.StatusMissing
|
||||
|
||||
// Special handling for hyprctl - it comes with system hyprland
|
||||
if tool.name == "hyprctl" {
|
||||
if n.commandExists("hyprctl") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
} else {
|
||||
if n.commandExists(tool.name) {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
}
|
||||
|
||||
dependencies = append(dependencies, deps.Dependency{
|
||||
Name: tool.name,
|
||||
Status: status,
|
||||
Description: tool.description,
|
||||
Required: true,
|
||||
})
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if n.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("nix", "profile", "list")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(output), pkg)
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
"git": {Name: "nixpkgs#git", Repository: RepoTypeSystem},
|
||||
"quickshell": {Name: "github:quickshell-mirror/quickshell", Repository: RepoTypeFlake},
|
||||
"matugen": {Name: "github:InioX/matugen", Repository: RepoTypeFlake},
|
||||
"dgop": {Name: "github:AvengeMedia/dgop", Repository: RepoTypeFlake},
|
||||
"dms (DankMaterialShell)": {Name: "github:AvengeMedia/DankMaterialShell", Repository: RepoTypeFlake},
|
||||
"ghostty": {Name: "nixpkgs#ghostty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "nixpkgs#alacritty", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "nixpkgs#cliphist", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "nixpkgs#wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "nixpkgs#xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "nixpkgs#mate.mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "nixpkgs#accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "nixpkgs#hyprpicker", Repository: RepoTypeSystem},
|
||||
}
|
||||
|
||||
// Note: Window managers (hyprland/niri) should be installed system-wide on NixOS
|
||||
// We only install the tools here
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
// Skip hyprland itself - should be installed system-wide
|
||||
packages["grim"] = PackageMapping{Name: "nixpkgs#grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "nixpkgs#slurp", Repository: RepoTypeSystem}
|
||||
packages["grimblast"] = PackageMapping{Name: "github:hyprwm/contrib#grimblast", Repository: RepoTypeFlake}
|
||||
packages["jq"] = PackageMapping{Name: "nixpkgs#jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
// Skip niri itself - should be installed system-wide
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "nixpkgs#xwayland-satellite", Repository: RepoTypeFlake}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "NixOS prerequisites ready",
|
||||
IsComplete: false,
|
||||
LogOutput: "NixOS package manager is ready to use",
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := n.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
nixpkgsPkgs, flakePkgs := n.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: Nixpkgs Packages
|
||||
if len(nixpkgsPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d packages from nixpkgs...", len(nixpkgsPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(nixpkgsPkgs, ", ")),
|
||||
}
|
||||
if err := n.installNixpkgsPackages(ctx, nixpkgsPkgs, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install nixpkgs packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Flake Packages
|
||||
if len(flakePkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d packages from flakes...", len(flakePkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing flake packages: %s", strings.Join(flakePkgs, ", ")),
|
||||
}
|
||||
if err := n.installFlakePackages(ctx, flakePkgs, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install flake packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
if err := n.postInstallConfig(progressChan); err != nil {
|
||||
return fmt.Errorf("failed to configure system: %w", err)
|
||||
}
|
||||
|
||||
// Phase 5: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string) {
|
||||
nixpkgsPkgs := []string{}
|
||||
flakePkgs := []string{}
|
||||
|
||||
packageMap := n.GetPackageMapping(wm)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
n.log(fmt.Sprintf("Warning: No package mapping found for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
nixpkgsPkgs = append(nixpkgsPkgs, pkgInfo.Name)
|
||||
case RepoTypeFlake:
|
||||
flakePkgs = append(flakePkgs, pkgInfo.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nixpkgsPkgs, flakePkgs
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) installNixpkgsPackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.log(fmt.Sprintf("Installing nixpkgs packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"profile", "install"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing nixpkgs packages...",
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("nix %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "nix", args...)
|
||||
return n.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) installFlakePackages(ctx context.Context, packages []string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.log(fmt.Sprintf("Installing flake packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
baseProgress := 0.65
|
||||
progressStep := 0.20 / float64(len(packages))
|
||||
|
||||
for i, pkg := range packages {
|
||||
currentProgress := baseProgress + (float64(i) * progressStep)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: currentProgress,
|
||||
Step: fmt.Sprintf("Installing flake package %s (%d/%d)...", pkg, i+1, len(packages)),
|
||||
IsComplete: false,
|
||||
CommandInfo: fmt.Sprintf("nix profile install %s", pkg),
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "nix", "profile", "install", pkg)
|
||||
if err := n.runWithProgress(cmd, progressChan, PhaseAURPackages, currentProgress, currentProgress+progressStep); err != nil {
|
||||
return fmt.Errorf("failed to install flake package %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NixOSDistribution) postInstallConfig(progressChan chan<- InstallProgressMsg) error {
|
||||
// For NixOS, DMS is installed as a flake package, so we skip both the binary installation and git clone
|
||||
// The flake installation handles both the binary and config files correctly
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.95,
|
||||
Step: "NixOS configuration complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "DMS installed via flake - binary and config handled by Nix",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
605
internal/distros/opensuse.go
Normal file
605
internal/distros/opensuse.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewOpenSUSEDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type OpenSUSEDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &OpenSUSEDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetID() string {
|
||||
return o.config.ID
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetColorHex() string {
|
||||
return o.config.ColorHex
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetFamily() DistroFamily {
|
||||
return o.config.Family
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerZypper
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return o.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, o.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, o.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, o.detectGit())
|
||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, o.detectQuickshell())
|
||||
dependencies = append(dependencies, o.detectXDGPortal())
|
||||
dependencies = append(dependencies, o.detectPolkitAgent())
|
||||
dependencies = append(dependencies, o.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, o.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, o.detectMatugen())
|
||||
dependencies = append(dependencies, o.detectDgop())
|
||||
dependencies = append(dependencies, o.detectHyprpicker())
|
||||
dependencies = append(dependencies, o.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("rpm", "-q", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard zypper packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypeSystem},
|
||||
|
||||
// Manual builds
|
||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if o.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||
return []string{
|
||||
"make",
|
||||
"unzip",
|
||||
"gcc",
|
||||
"gcc-c++",
|
||||
"cmake",
|
||||
"ninja",
|
||||
"pkgconf-pkg-config",
|
||||
"git",
|
||||
"qt6-base-devel",
|
||||
"qt6-declarative-devel",
|
||||
"qt6-declarative-private-devel",
|
||||
"qt6-shadertools",
|
||||
"qt6-shadertools-devel",
|
||||
"qt6-wayland-devel",
|
||||
"qt6-waylandclient-private-devel",
|
||||
"spirv-tools-devel",
|
||||
"cli11-devel",
|
||||
"wayland-protocols-devel",
|
||||
"libgbm-devel",
|
||||
"libdrm-devel",
|
||||
"pipewire-devel",
|
||||
"jemalloc-devel",
|
||||
"wayland-utils",
|
||||
"Mesa-libGLESv3-devel",
|
||||
"pam-devel",
|
||||
"glib2-devel",
|
||||
"polkit-devel",
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
prerequisites := o.getPrerequisites()
|
||||
var missingPkgs []string
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Checking prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Checking prerequisite packages",
|
||||
}
|
||||
|
||||
for _, pkg := range prerequisites {
|
||||
checkCmd := exec.CommandContext(ctx, "rpm", "-q", pkg)
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
missingPkgs = append(missingPkgs, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
_, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
o.log("go not found in PATH, will install go")
|
||||
missingPkgs = append(missingPkgs, "go")
|
||||
} else {
|
||||
o.log("go already available in PATH")
|
||||
}
|
||||
|
||||
if len(missingPkgs) == 0 {
|
||||
o.log("All prerequisites already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo zypper install -y %s", strings.Join(missingPkgs, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.logError("failed to install prerequisites", err)
|
||||
o.log(fmt.Sprintf("Prerequisites command output: %s", string(output)))
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
o.log(fmt.Sprintf("Prerequisites install output: %s", string(output)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, manualPkgs := o.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: System Packages (Zypper)
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := o.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 5: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []string) {
|
||||
systemPkgs := []string{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
variantMap := make(map[string]deps.PackageVariant)
|
||||
for _, dep := range dependencies {
|
||||
variantMap[dep.Name] = dep.Variant
|
||||
}
|
||||
|
||||
packageMap := o.GetPackageMappingWithVariants(wm, variantMap)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
o.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"zypper", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
// installQuickshell overrides the base implementation to set openSUSE-specific CFLAGS
|
||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
o.log("Installing quickshell from source (with openSUSE-specific build flags)...")
|
||||
|
||||
homeDir := os.Getenv("HOME")
|
||||
if homeDir == "" {
|
||||
return fmt.Errorf("HOME environment variable not set")
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Cloning quickshell repository...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/quickshell-mirror/quickshell.git",
|
||||
}
|
||||
|
||||
var cloneCmd *exec.Cmd
|
||||
if forceQuickshellGit {
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
// Get latest tag from repository
|
||||
latestTag := o.getLatestQuickshellTag(ctx)
|
||||
if latestTag != "" {
|
||||
o.log(fmt.Sprintf("Using latest quickshell tag: %s", latestTag))
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "--branch", latestTag, "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
} else {
|
||||
o.log("Warning: failed to fetch latest tag, using default branch")
|
||||
cloneCmd = exec.CommandContext(ctx, "git", "clone", "https://github.com/quickshell-mirror/quickshell.git", tmpDir)
|
||||
}
|
||||
}
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone quickshell: %w", err)
|
||||
}
|
||||
|
||||
buildDir := tmpDir + "/build"
|
||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create build directory: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.3,
|
||||
Step: "Configuring quickshell build (with openSUSE flags)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake -B build -S . -G Ninja",
|
||||
}
|
||||
|
||||
// Get optflags from rpm
|
||||
optflagsCmd := exec.CommandContext(ctx, "rpm", "--eval", "%{optflags}")
|
||||
optflagsOutput, err := optflagsCmd.Output()
|
||||
optflags := strings.TrimSpace(string(optflagsOutput))
|
||||
if err != nil || optflags == "" {
|
||||
o.log("Warning: Could not get optflags from rpm, using default -O2 -g")
|
||||
optflags = "-O2 -g"
|
||||
}
|
||||
|
||||
// Set openSUSE-specific CFLAGS
|
||||
customCFLAGS := fmt.Sprintf("%s -I/usr/include/wayland", optflags)
|
||||
|
||||
configureCmd := exec.CommandContext(ctx, "cmake", "-GNinja", "-B", "build",
|
||||
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||
"-DCRASH_REPORTER=off",
|
||||
"-DCMAKE_CXX_STANDARD=20")
|
||||
configureCmd.Dir = tmpDir
|
||||
configureCmd.Env = append(os.Environ(),
|
||||
"TMPDIR="+cacheDir,
|
||||
"CFLAGS="+customCFLAGS,
|
||||
"CXXFLAGS="+customCFLAGS)
|
||||
|
||||
o.log(fmt.Sprintf("Using CFLAGS: %s", customCFLAGS))
|
||||
|
||||
output, err := configureCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
o.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
||||
return fmt.Errorf("failed to configure quickshell: %w\nCMake output:\n%s", err, string(output))
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("cmake configure successful. Output:\n%s", string(output)))
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.4,
|
||||
Step: "Building quickshell (this may take a while)...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "cmake --build build",
|
||||
}
|
||||
|
||||
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "build")
|
||||
buildCmd.Dir = tmpDir
|
||||
buildCmd.Env = append(os.Environ(),
|
||||
"TMPDIR="+cacheDir,
|
||||
"CFLAGS="+customCFLAGS,
|
||||
"CXXFLAGS="+customCFLAGS)
|
||||
if err := o.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.4, 0.8, "Building quickshell..."); err != nil {
|
||||
return fmt.Errorf("failed to build quickshell: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.8,
|
||||
Step: "Installing quickshell...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo cmake --install build",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("cd %s && cmake --install build", tmpDir))
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
|
||||
o.log("quickshell installed successfully from source")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if o.commandExists("cargo") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.82,
|
||||
Step: "Installing rustup...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo zypper install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := execSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.83,
|
||||
Step: "Installing stable Rust toolchain...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "rustup install stable",
|
||||
}
|
||||
|
||||
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||
if err := o.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||
}
|
||||
|
||||
if !o.commandExists("cargo") {
|
||||
o.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallManualPackages overrides the base implementation to use openSUSE-specific builds
|
||||
func (o *OpenSUSEDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
// Install Rust if needed for matugen
|
||||
for _, pkg := range packages {
|
||||
if pkg == "matugen" {
|
||||
if err := o.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == "quickshell" {
|
||||
if err := o.installQuickshell(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Use the base ManualPackageInstaller for other packages
|
||||
if err := o.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
115
internal/distros/osinfo.go
Normal file
115
internal/distros/osinfo.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/errdefs"
|
||||
)
|
||||
|
||||
// DistroInfo contains basic information about a distribution
|
||||
type DistroInfo struct {
|
||||
ID string
|
||||
HexColorCode string
|
||||
}
|
||||
|
||||
// OSInfo contains complete OS information
|
||||
type OSInfo struct {
|
||||
Distribution DistroInfo
|
||||
Version string
|
||||
VersionID string
|
||||
PrettyName string
|
||||
Architecture string
|
||||
}
|
||||
|
||||
// GetOSInfo detects the current OS and returns information about it
|
||||
func GetOSInfo() (*OSInfo, error) {
|
||||
if runtime.GOOS != "linux" {
|
||||
return nil, errdefs.NewCustomError(errdefs.ErrTypeNotLinux, fmt.Sprintf("Only linux is supported, but I found %s", runtime.GOOS))
|
||||
}
|
||||
|
||||
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
|
||||
return nil, errdefs.NewCustomError(errdefs.ErrTypeInvalidArchitecture, fmt.Sprintf("Only amd64 and arm64 are supported, but I found %s", runtime.GOARCH))
|
||||
}
|
||||
|
||||
info := &OSInfo{
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
|
||||
file, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := parts[0]
|
||||
value := strings.Trim(parts[1], "\"")
|
||||
|
||||
switch key {
|
||||
case "ID":
|
||||
config, exists := Registry[value]
|
||||
if !exists {
|
||||
return nil, errdefs.NewCustomError(errdefs.ErrTypeUnsupportedDistribution, fmt.Sprintf("Unsupported distribution: %s", value))
|
||||
}
|
||||
|
||||
info.Distribution = DistroInfo{
|
||||
ID: value, // Use the actual ID from os-release
|
||||
HexColorCode: config.ColorHex,
|
||||
}
|
||||
case "VERSION_ID", "BUILD_ID":
|
||||
info.VersionID = value
|
||||
case "VERSION":
|
||||
info.Version = value
|
||||
case "PRETTY_NAME":
|
||||
info.PrettyName = value
|
||||
}
|
||||
}
|
||||
|
||||
return info, scanner.Err()
|
||||
}
|
||||
|
||||
// IsUnsupportedDistro checks if a distribution/version combination is supported
|
||||
func IsUnsupportedDistro(distroID, versionID string) bool {
|
||||
if !IsDistroSupported(distroID) {
|
||||
return true
|
||||
}
|
||||
|
||||
if distroID == "ubuntu" {
|
||||
parts := strings.Split(versionID, ".")
|
||||
if len(parts) >= 2 {
|
||||
major, err1 := strconv.Atoi(parts[0])
|
||||
minor, err2 := strconv.Atoi(parts[1])
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
return major < 25 || (major == 25 && minor < 4)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if distroID == "debian" {
|
||||
if versionID == "" {
|
||||
// debian testing/sid have no version ID
|
||||
return false
|
||||
}
|
||||
versionNum, err := strconv.Atoi(versionID)
|
||||
if err == nil {
|
||||
return versionNum < 12
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
752
internal/distros/ubuntu.go
Normal file
752
internal/distros/ubuntu.go
Normal file
@@ -0,0 +1,752 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("ubuntu", "#E95420", FamilyUbuntu, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewUbuntuDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type UbuntuDistribution struct {
|
||||
*BaseDistribution
|
||||
*ManualPackageInstaller
|
||||
config DistroConfig
|
||||
}
|
||||
|
||||
func NewUbuntuDistribution(config DistroConfig, logChan chan<- string) *UbuntuDistribution {
|
||||
base := NewBaseDistribution(logChan)
|
||||
return &UbuntuDistribution{
|
||||
BaseDistribution: base,
|
||||
ManualPackageInstaller: &ManualPackageInstaller{BaseDistribution: base},
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetID() string {
|
||||
return u.config.ID
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetColorHex() string {
|
||||
return u.config.ColorHex
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetFamily() DistroFamily {
|
||||
return u.config.Family
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetPackageManager() PackageManagerType {
|
||||
return PackageManagerAPT
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) DetectDependencies(ctx context.Context, wm deps.WindowManager) ([]deps.Dependency, error) {
|
||||
return u.DetectDependenciesWithTerminal(ctx, wm, deps.TerminalGhostty)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]deps.Dependency, error) {
|
||||
var dependencies []deps.Dependency
|
||||
|
||||
// DMS at the top (shell is prominent)
|
||||
dependencies = append(dependencies, u.detectDMS())
|
||||
|
||||
// Terminal with choice support
|
||||
dependencies = append(dependencies, u.detectSpecificTerminal(terminal))
|
||||
|
||||
// Common detections using base methods
|
||||
dependencies = append(dependencies, u.detectGit())
|
||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||
dependencies = append(dependencies, u.detectQuickshell())
|
||||
dependencies = append(dependencies, u.detectXDGPortal())
|
||||
dependencies = append(dependencies, u.detectPolkitAgent())
|
||||
dependencies = append(dependencies, u.detectAccountsService())
|
||||
|
||||
// Hyprland-specific tools
|
||||
if wm == deps.WindowManagerHyprland {
|
||||
dependencies = append(dependencies, u.detectHyprlandTools()...)
|
||||
}
|
||||
|
||||
// Niri-specific tools
|
||||
if wm == deps.WindowManagerNiri {
|
||||
dependencies = append(dependencies, u.detectXwaylandSatellite())
|
||||
}
|
||||
|
||||
// Base detections (common across distros)
|
||||
dependencies = append(dependencies, u.detectMatugen())
|
||||
dependencies = append(dependencies, u.detectDgop())
|
||||
dependencies = append(dependencies, u.detectHyprpicker())
|
||||
dependencies = append(dependencies, u.detectClipboardTools()...)
|
||||
|
||||
return dependencies, nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectXDGPortal() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.packageInstalled("xdg-desktop-portal-gtk") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xdg-desktop-portal-gtk",
|
||||
Status: status,
|
||||
Description: "Desktop integration portal for GTK",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectPolkitAgent() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.packageInstalled("mate-polkit") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "mate-polkit",
|
||||
Status: status,
|
||||
Description: "PolicyKit authentication agent",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.commandExists("xwayland-satellite") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "xwayland-satellite",
|
||||
Status: status,
|
||||
Description: "Xwayland support",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
||||
status := deps.StatusMissing
|
||||
if u.packageInstalled("accountsservice") {
|
||||
status = deps.StatusInstalled
|
||||
}
|
||||
|
||||
return deps.Dependency{
|
||||
Name: "accountsservice",
|
||||
Status: status,
|
||||
Description: "D-Bus interface for user account query and manipulation",
|
||||
Required: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||
cmd := exec.Command("dpkg", "-l", pkg)
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard APT packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
"mate-polkit": {Name: "mate-polkit", Repository: RepoTypeSystem},
|
||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||
"hyprpicker": {Name: "hyprpicker", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"},
|
||||
|
||||
// Manual builds (niri and quickshell likely not available in Ubuntu repos or PPAs)
|
||||
"dms (DankMaterialShell)": {Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"},
|
||||
"niri": {Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"},
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeManual, BuildFunc: "installQuickshell"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeManual, BuildFunc: "installGhostty"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeManual, BuildFunc: "installMatugen"},
|
||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||
"cliphist": {Name: "cliphist", Repository: RepoTypeManual, BuildFunc: "installCliphist"},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
case deps.WindowManagerHyprland:
|
||||
// Use the cppiber PPA for Hyprland
|
||||
packages["hyprland"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||
packages["grim"] = PackageMapping{Name: "grim", Repository: RepoTypeSystem}
|
||||
packages["slurp"] = PackageMapping{Name: "slurp", Repository: RepoTypeSystem}
|
||||
packages["hyprctl"] = PackageMapping{Name: "hyprland", Repository: RepoTypePPA, RepoURL: "ppa:cppiber/hyprland"}
|
||||
packages["grimblast"] = PackageMapping{Name: "grimblast", Repository: RepoTypeManual, BuildFunc: "installGrimblast"}
|
||||
packages["jq"] = PackageMapping{Name: "jq", Repository: RepoTypeSystem}
|
||||
case deps.WindowManagerNiri:
|
||||
packages["niri"] = PackageMapping{Name: "niri", Repository: RepoTypeManual, BuildFunc: "installNiri"}
|
||||
packages["xwayland-satellite"] = PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeManual, BuildFunc: "installXwaylandSatellite"}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.06,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Updating APT package lists",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.08,
|
||||
Step: "Installing build-essential...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y build-essential",
|
||||
LogOutput: "Installing build tools",
|
||||
}
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
// Not installed, install it
|
||||
cmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.10,
|
||||
Step: "Installing development dependencies...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev",
|
||||
LogOutput: "Installing additional development tools",
|
||||
}
|
||||
|
||||
devToolsCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||
return fmt.Errorf("failed to install development tools: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.12,
|
||||
Step: "Prerequisites installation complete",
|
||||
IsComplete: false,
|
||||
LogOutput: "Prerequisites successfully installed",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) InstallPackages(ctx context.Context, dependencies []deps.Dependency, wm deps.WindowManager, sudoPassword string, reinstallFlags map[string]bool, progressChan chan<- InstallProgressMsg) error {
|
||||
// Phase 1: Check Prerequisites
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
Progress: 0.05,
|
||||
Step: "Checking system prerequisites...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting prerequisite check...",
|
||||
}
|
||||
|
||||
if err := u.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||
}
|
||||
|
||||
systemPkgs, ppaPkgs, manualPkgs := u.categorizePackages(dependencies, wm, reinstallFlags)
|
||||
|
||||
// Phase 2: Enable PPA repositories
|
||||
if len(ppaPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.15,
|
||||
Step: "Enabling PPA repositories...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Setting up PPA repositories for additional packages",
|
||||
}
|
||||
if err := u.enablePPARepos(ctx, ppaPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to enable PPA repositories: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: System Packages (APT)
|
||||
if len(systemPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.35,
|
||||
Step: fmt.Sprintf("Installing %d system packages...", len(systemPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||
}
|
||||
if err := u.installAPTPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install APT packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: PPA Packages
|
||||
ppaPkgNames := u.extractPackageNames(ppaPkgs)
|
||||
if len(ppaPkgNames) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages, // Reusing AUR phase for PPA
|
||||
Progress: 0.65,
|
||||
Step: fmt.Sprintf("Installing %d PPA packages...", len(ppaPkgNames)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Installing PPA packages: %s", strings.Join(ppaPkgNames, ", ")),
|
||||
}
|
||||
if err := u.installPPAPackages(ctx, ppaPkgNames, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install PPA packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Manual Builds
|
||||
if len(manualPkgs) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.80,
|
||||
Step: "Installing build dependencies...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Installing build tools for manual compilation",
|
||||
}
|
||||
if err := u.installBuildDependencies(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install build dependencies: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.85,
|
||||
Step: fmt.Sprintf("Building %d packages from source...", len(manualPkgs)),
|
||||
IsComplete: false,
|
||||
LogOutput: fmt.Sprintf("Building from source: %s", strings.Join(manualPkgs, ", ")),
|
||||
}
|
||||
if err := u.InstallManualPackages(ctx, manualPkgs, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install manual packages: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 6: Configuration
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseConfiguration,
|
||||
Progress: 0.90,
|
||||
Step: "Configuring system...",
|
||||
IsComplete: false,
|
||||
LogOutput: "Starting post-installation configuration...",
|
||||
}
|
||||
|
||||
// Phase 7: Complete
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseComplete,
|
||||
Progress: 1.0,
|
||||
Step: "Installation complete!",
|
||||
IsComplete: true,
|
||||
LogOutput: "All packages installed and configured successfully",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) categorizePackages(dependencies []deps.Dependency, wm deps.WindowManager, reinstallFlags map[string]bool) ([]string, []PackageMapping, []string) {
|
||||
systemPkgs := []string{}
|
||||
ppaPkgs := []PackageMapping{}
|
||||
manualPkgs := []string{}
|
||||
|
||||
packageMap := u.GetPackageMapping(wm)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// Skip installed packages unless marked for reinstall
|
||||
if dep.Status == deps.StatusInstalled && !reinstallFlags[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfo, exists := packageMap[dep.Name]
|
||||
if !exists {
|
||||
u.log(fmt.Sprintf("Warning: No package mapping for %s", dep.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
switch pkgInfo.Repository {
|
||||
case RepoTypeSystem:
|
||||
systemPkgs = append(systemPkgs, pkgInfo.Name)
|
||||
case RepoTypePPA:
|
||||
ppaPkgs = append(ppaPkgs, pkgInfo)
|
||||
case RepoTypeManual:
|
||||
manualPkgs = append(manualPkgs, dep.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return systemPkgs, ppaPkgs, manualPkgs
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||
names := make([]string, len(packages))
|
||||
for i, pkg := range packages {
|
||||
names[i] = pkg.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
installPPACmd := execSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y software-properties-common")
|
||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||
}
|
||||
|
||||
for _, pkg := range ppaPkgs {
|
||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||
u.log(fmt.Sprintf("Enabling PPA repository: %s", pkg.RepoURL))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.20,
|
||||
Step: fmt.Sprintf("Enabling PPA repo %s...", pkg.RepoURL),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||
return fmt.Errorf("failed to enable PPA repo %s: %w", pkg.RepoURL, err)
|
||||
}
|
||||
u.log(fmt.Sprintf("PPA repo %s enabled successfully", pkg.RepoURL))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(enabledRepos) > 0 {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.25,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.40,
|
||||
Step: "Installing system packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, packages...)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
Progress: 0.70,
|
||||
Step: "Installing PPA packages...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
}
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
buildDeps := make(map[string]bool)
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri":
|
||||
buildDeps["curl"] = true
|
||||
buildDeps["libxkbcommon-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["libudev-dev"] = true
|
||||
buildDeps["libinput-dev"] = true
|
||||
buildDeps["libdisplay-info-dev"] = true
|
||||
buildDeps["libpango1.0-dev"] = true
|
||||
buildDeps["libcairo-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libc6-dev"] = true
|
||||
buildDeps["clang"] = true
|
||||
buildDeps["libseat-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["alacritty"] = true
|
||||
buildDeps["fuzzel"] = true
|
||||
buildDeps["libxcb-cursor-dev"] = true
|
||||
case "quickshell":
|
||||
buildDeps["qt6-base-dev"] = true
|
||||
buildDeps["qt6-base-private-dev"] = true
|
||||
buildDeps["qt6-declarative-dev"] = true
|
||||
buildDeps["qt6-declarative-private-dev"] = true
|
||||
buildDeps["qt6-wayland-dev"] = true
|
||||
buildDeps["qt6-wayland-private-dev"] = true
|
||||
buildDeps["qt6-tools-dev"] = true
|
||||
buildDeps["libqt6svg6-dev"] = true
|
||||
buildDeps["qt6-shadertools-dev"] = true
|
||||
buildDeps["spirv-tools"] = true
|
||||
buildDeps["libcli11-dev"] = true
|
||||
buildDeps["libjemalloc-dev"] = true
|
||||
buildDeps["libwayland-dev"] = true
|
||||
buildDeps["wayland-protocols"] = true
|
||||
buildDeps["libdrm-dev"] = true
|
||||
buildDeps["libgbm-dev"] = true
|
||||
buildDeps["libegl-dev"] = true
|
||||
buildDeps["libgles2-mesa-dev"] = true
|
||||
buildDeps["libgl1-mesa-dev"] = true
|
||||
buildDeps["libxcb1-dev"] = true
|
||||
buildDeps["libpipewire-0.3-dev"] = true
|
||||
buildDeps["libpam0g-dev"] = true
|
||||
case "ghostty":
|
||||
buildDeps["curl"] = true
|
||||
buildDeps["libgtk-4-dev"] = true
|
||||
buildDeps["libadwaita-1-dev"] = true
|
||||
case "matugen":
|
||||
buildDeps["curl"] = true
|
||||
case "cliphist":
|
||||
// Go will be installed separately with PPA
|
||||
}
|
||||
}
|
||||
|
||||
for _, pkg := range manualPkgs {
|
||||
switch pkg {
|
||||
case "niri", "matugen":
|
||||
if err := u.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Rust: %w", err)
|
||||
}
|
||||
case "ghostty":
|
||||
if err := u.installZig(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Zig: %w", err)
|
||||
}
|
||||
case "cliphist", "dgop":
|
||||
if err := u.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install Go: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(buildDeps) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
depList := make([]string, 0, len(buildDeps))
|
||||
for dep := range buildDeps {
|
||||
depList = append(depList, dep)
|
||||
}
|
||||
|
||||
args := []string{"apt-get", "install", "-y"}
|
||||
args = append(args, depList...)
|
||||
|
||||
cmd := execSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if u.commandExists("cargo") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.82,
|
||||
Step: "Installing rustup...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install rustup",
|
||||
}
|
||||
|
||||
rustupInstallCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||
return fmt.Errorf("failed to install rustup: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.83,
|
||||
Step: "Installing stable Rust toolchain...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "rustup install stable",
|
||||
}
|
||||
|
||||
rustInstallCmd := exec.CommandContext(ctx, "bash", "-c", "rustup install stable && rustup default stable")
|
||||
if err := u.runWithProgress(rustInstallCmd, progressChan, PhaseSystemPackages, 0.83, 0.84); err != nil {
|
||||
return fmt.Errorf("failed to install Rust toolchain: %w", err)
|
||||
}
|
||||
|
||||
// Verify cargo is now available
|
||||
if !u.commandExists("cargo") {
|
||||
u.log("Warning: cargo not found in PATH after Rust installation, trying to source environment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installZig(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if u.commandExists("zig") {
|
||||
return nil
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
zigUrl := "https://ziglang.org/download/0.11.0/zig-linux-x86_64-0.11.0.tar.xz"
|
||||
zigTmp := filepath.Join(cacheDir, "zig.tar.xz")
|
||||
|
||||
downloadCmd := exec.CommandContext(ctx, "curl", "-L", zigUrl, "-o", zigTmp)
|
||||
if err := u.runWithProgress(downloadCmd, progressChan, PhaseSystemPackages, 0.84, 0.85); err != nil {
|
||||
return fmt.Errorf("failed to download Zig: %w", err)
|
||||
}
|
||||
|
||||
extractCmd := execSudoCommand(ctx, sudoPassword,
|
||||
fmt.Sprintf("tar -xf %s -C /opt/", zigTmp))
|
||||
if err := u.runWithProgress(extractCmd, progressChan, PhaseSystemPackages, 0.85, 0.86); err != nil {
|
||||
return fmt.Errorf("failed to extract Zig: %w", err)
|
||||
}
|
||||
|
||||
linkCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"ln -sf /opt/zig-linux-x86_64-0.11.0/zig /usr/local/bin/zig")
|
||||
return u.runWithProgress(linkCmd, progressChan, PhaseSystemPackages, 0.86, 0.87)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if u.commandExists("go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.87,
|
||||
Step: "Adding Go PPA repository...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||
}
|
||||
|
||||
addPPACmd := execSudoCommand(ctx, sudoPassword,
|
||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.88,
|
||||
Step: "Updating package lists...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
|
||||
updateCmd := execSudoCommand(ctx, sudoPassword, "apt-get update")
|
||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||
}
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.89,
|
||||
Step: "Installing Go...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get install golang-go",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) installGhosttyUbuntu(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
u.log("Installing Ghostty using Ubuntu installer script...")
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.1,
|
||||
Step: "Running Ghostty Ubuntu installer...",
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh | sudo bash",
|
||||
LogOutput: "Installing Ghostty using pre-built Ubuntu package",
|
||||
}
|
||||
|
||||
installCmd := execSudoCommand(ctx, sudoPassword,
|
||||
"/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/mkasberg/ghostty-ubuntu/HEAD/install.sh)\"")
|
||||
|
||||
if err := u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.9); err != nil {
|
||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||
}
|
||||
|
||||
u.log("Ghostty installed successfully using Ubuntu installer")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Override InstallManualPackages for Ubuntu to handle Ubuntu-specific installations
|
||||
func (u *UbuntuDistribution) InstallManualPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if len(packages) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
u.log(fmt.Sprintf("Installing manual packages: %s", strings.Join(packages, ", ")))
|
||||
|
||||
for _, pkg := range packages {
|
||||
switch pkg {
|
||||
case "ghostty":
|
||||
if err := u.installGhosttyUbuntu(ctx, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||
}
|
||||
default:
|
||||
// Use the base ManualPackageInstaller for other packages
|
||||
if err := u.ManualPackageInstaller.InstallManualPackages(ctx, []string{pkg}, sudoPassword, progressChan); err != nil {
|
||||
return fmt.Errorf("failed to install %s: %w", pkg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
438
internal/dms/app.go
Normal file
438
internal/dms/app.go
Normal file
@@ -0,0 +1,438 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateUpdate
|
||||
StateUpdatePassword
|
||||
StateUpdateProgress
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateGreeterMenu
|
||||
StateGreeterCompositorSelect
|
||||
StateGreeterPassword
|
||||
StateGreeterInstalling
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
updateDeps []DependencyInfo
|
||||
selectedUpdateDep int
|
||||
updateToggles map[string]bool
|
||||
|
||||
updateProgressChan chan updateProgressMsg
|
||||
updateProgress updateProgressMsg
|
||||
updateLogs []string
|
||||
sudoPassword string
|
||||
passwordInput string
|
||||
passwordError string
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
selectedGreeterItem int
|
||||
greeterInstallChan chan greeterProgressMsg
|
||||
greeterProgress greeterProgressMsg
|
||||
greeterLogs []string
|
||||
greeterPasswordInput string
|
||||
greeterPasswordError string
|
||||
greeterSudoPassword string
|
||||
greeterCompositors []string
|
||||
greeterSelectedComp int
|
||||
greeterChosenCompositor string
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
dependencies := detector.GetInstalledComponents()
|
||||
|
||||
// Use the proper detection method for both window managers
|
||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
||||
if err != nil {
|
||||
// Fallback to false if detection fails
|
||||
hyprlandInstalled = false
|
||||
niriInstalled = false
|
||||
}
|
||||
|
||||
updateToggles := make(map[string]bool)
|
||||
for _, dep := range dependencies {
|
||||
if dep.Name == "dms (DankMaterialShell)" && dep.Status == deps.StatusNeedsUpdate {
|
||||
updateToggles[dep.Name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
updateToggles: updateToggles,
|
||||
updateDeps: dependencies,
|
||||
updateProgressChan: make(chan updateProgressMsg, 100),
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
greeterInstallChan: make(chan greeterProgressMsg, 100),
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{
|
||||
{Label: "Update", Action: StateUpdate},
|
||||
}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
// Greeter management
|
||||
items = append(items, MenuItem{Label: "Greeter", Action: StateGreeterMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
// Check for both -c and -p flag patterns since quickshell can be started either way
|
||||
// -c dms: config name mode
|
||||
// -p <path>/dms: path mode (used when installed via system packages)
|
||||
cmd := exec.Command("pgrep", "-f", "qs.*dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case shellStartedMsg:
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
return m, nil
|
||||
case updateProgressMsg:
|
||||
m.updateProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.updateLogs = append(m.updateLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForProgress()
|
||||
case updateCompleteMsg:
|
||||
m.updateProgress.complete = true
|
||||
m.updateProgress.err = msg.err
|
||||
m.dependencies = m.detector.GetInstalledComponents()
|
||||
m.updateDeps = m.dependencies
|
||||
m.menuItems = m.buildMenuItems()
|
||||
|
||||
// Restart shell if update was successful and shell is running
|
||||
if msg.err == nil && m.isShellRunning() {
|
||||
restartShell()
|
||||
}
|
||||
return m, nil
|
||||
case greeterProgressMsg:
|
||||
m.greeterProgress = msg
|
||||
if msg.logOutput != "" {
|
||||
m.greeterLogs = append(m.greeterLogs, msg.logOutput)
|
||||
}
|
||||
return m, m.waitForGreeterProgress()
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case greeterPasswordValidMsg:
|
||||
if msg.valid {
|
||||
m.greeterSudoPassword = msg.password
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
m.state = StateGreeterInstalling
|
||||
m.greeterProgress = greeterProgressMsg{step: "Starting greeter installation..."}
|
||||
m.greeterLogs = []string{}
|
||||
return m, tea.Batch(m.performGreeterInstall(), m.waitForGreeterProgress())
|
||||
} else {
|
||||
m.greeterPasswordError = "Incorrect password. Please try again."
|
||||
m.greeterPasswordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case passwordValidMsg:
|
||||
if msg.valid {
|
||||
m.sudoPassword = msg.password
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
m.state = StateUpdateProgress
|
||||
m.updateProgress = updateProgressMsg{progress: 0.0, step: "Starting update..."}
|
||||
m.updateLogs = []string{}
|
||||
return m, tea.Batch(m.performUpdate(), m.waitForProgress())
|
||||
} else {
|
||||
m.passwordError = "Incorrect password. Please try again."
|
||||
m.passwordInput = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateUpdate:
|
||||
return m.updateUpdateView(msg)
|
||||
case StateUpdatePassword:
|
||||
return m.updatePasswordView(msg)
|
||||
case StateUpdateProgress:
|
||||
return m.updateProgressView(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateGreeterMenu:
|
||||
return m.updateGreeterMenu(msg)
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.updateGreeterCompositorSelect(msg)
|
||||
case StateGreeterPassword:
|
||||
return m.updateGreeterPasswordView(msg)
|
||||
case StateGreeterInstalling:
|
||||
return m.updateGreeterInstalling(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type updateProgressMsg struct {
|
||||
progress float64
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type updateCompleteMsg struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type passwordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
type greeterProgressMsg struct {
|
||||
step string
|
||||
complete bool
|
||||
err error
|
||||
logOutput string
|
||||
}
|
||||
|
||||
type greeterPasswordValidMsg struct {
|
||||
password string
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (m Model) waitForProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.updateProgressChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) waitForGreeterProgress() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return <-m.greeterInstallChan
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateUpdate:
|
||||
return m.renderUpdateView()
|
||||
case StateUpdatePassword:
|
||||
return m.renderPasswordView()
|
||||
case StateUpdateProgress:
|
||||
return m.renderProgressView()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateGreeterMenu:
|
||||
return m.renderGreeterMenu()
|
||||
case StateGreeterCompositorSelect:
|
||||
return m.renderGreeterCompositorSelect()
|
||||
case StateGreeterPassword:
|
||||
return m.renderGreeterPasswordView()
|
||||
case StateGreeterInstalling:
|
||||
return m.renderGreeterInstalling()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
261
internal/dms/app_distro.go
Normal file
261
internal/dms/app_distro.go
Normal file
@@ -0,0 +1,261 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateMainMenu AppState = iota
|
||||
StateShell
|
||||
StatePluginsMenu
|
||||
StatePluginsBrowse
|
||||
StatePluginDetail
|
||||
StatePluginSearch
|
||||
StatePluginsInstalled
|
||||
StatePluginInstalledDetail
|
||||
StateAbout
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
version string
|
||||
detector *Detector
|
||||
dependencies []DependencyInfo
|
||||
state AppState
|
||||
selectedItem int
|
||||
width int
|
||||
height int
|
||||
|
||||
// Menu items
|
||||
menuItems []MenuItem
|
||||
|
||||
// Window manager states
|
||||
hyprlandInstalled bool
|
||||
niriInstalled bool
|
||||
|
||||
pluginsMenuItems []MenuItem
|
||||
selectedPluginsMenuItem int
|
||||
pluginsList []pluginInfo
|
||||
filteredPluginsList []pluginInfo
|
||||
selectedPluginIndex int
|
||||
pluginsLoading bool
|
||||
pluginsError string
|
||||
pluginSearchQuery string
|
||||
installedPluginsList []pluginInfo
|
||||
selectedInstalledIndex int
|
||||
installedPluginsLoading bool
|
||||
installedPluginsError string
|
||||
pluginInstallStatus map[string]bool
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Category string
|
||||
Author string
|
||||
Description string
|
||||
Repo string
|
||||
Path string
|
||||
Capabilities []string
|
||||
Compositors []string
|
||||
Dependencies []string
|
||||
FirstParty bool
|
||||
}
|
||||
|
||||
type MenuItem struct {
|
||||
Label string
|
||||
Action AppState
|
||||
}
|
||||
|
||||
func NewModel(version string) Model {
|
||||
detector, _ := NewDetector()
|
||||
dependencies := detector.GetInstalledComponents()
|
||||
|
||||
// Use the proper detection method for both window managers
|
||||
hyprlandInstalled, niriInstalled, err := detector.GetWindowManagerStatus()
|
||||
if err != nil {
|
||||
// Fallback to false if detection fails
|
||||
hyprlandInstalled = false
|
||||
niriInstalled = false
|
||||
}
|
||||
|
||||
m := Model{
|
||||
version: version,
|
||||
detector: detector,
|
||||
dependencies: dependencies,
|
||||
state: StateMainMenu,
|
||||
selectedItem: 0,
|
||||
hyprlandInstalled: hyprlandInstalled,
|
||||
niriInstalled: niriInstalled,
|
||||
pluginInstallStatus: make(map[string]bool),
|
||||
}
|
||||
|
||||
m.menuItems = m.buildMenuItems()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) buildMenuItems() []MenuItem {
|
||||
items := []MenuItem{}
|
||||
|
||||
// Shell management
|
||||
if m.isShellRunning() {
|
||||
items = append(items, MenuItem{Label: "Terminate Shell", Action: StateShell})
|
||||
} else {
|
||||
items = append(items, MenuItem{Label: "Start Shell (Daemon)", Action: StateShell})
|
||||
}
|
||||
|
||||
// Plugins management
|
||||
items = append(items, MenuItem{Label: "Plugins", Action: StatePluginsMenu})
|
||||
|
||||
items = append(items, MenuItem{Label: "About", Action: StateAbout})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (m *Model) buildPluginsMenuItems() []MenuItem {
|
||||
return []MenuItem{
|
||||
{Label: "Browse Plugins", Action: StatePluginsBrowse},
|
||||
{Label: "View Installed", Action: StatePluginsInstalled},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) isShellRunning() bool {
|
||||
cmd := exec.Command("pgrep", "-f", "qs -c dms")
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
case pluginsLoadedMsg:
|
||||
m.pluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.pluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
m.updatePluginInstallStatus()
|
||||
}
|
||||
return m, nil
|
||||
case installedPluginsLoadedMsg:
|
||||
m.installedPluginsLoading = false
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.installedPluginsList = make([]pluginInfo, len(msg.plugins))
|
||||
for i, p := range msg.plugins {
|
||||
m.installedPluginsList[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
m.selectedInstalledIndex = 0
|
||||
}
|
||||
return m, nil
|
||||
case pluginUninstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.installedPluginsError = msg.err.Error()
|
||||
m.state = StatePluginInstalledDetail
|
||||
} else {
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
return m, nil
|
||||
case pluginInstalledMsg:
|
||||
if msg.err != nil {
|
||||
m.pluginsError = msg.err.Error()
|
||||
} else {
|
||||
m.pluginInstallStatus[msg.pluginName] = true
|
||||
m.pluginsError = ""
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.updateMainMenu(msg)
|
||||
case StateShell:
|
||||
return m.updateShellView(msg)
|
||||
case StatePluginsMenu:
|
||||
return m.updatePluginsMenu(msg)
|
||||
case StatePluginsBrowse:
|
||||
return m.updatePluginsBrowse(msg)
|
||||
case StatePluginDetail:
|
||||
return m.updatePluginDetail(msg)
|
||||
case StatePluginSearch:
|
||||
return m.updatePluginSearch(msg)
|
||||
case StatePluginsInstalled:
|
||||
return m.updatePluginsInstalled(msg)
|
||||
case StatePluginInstalledDetail:
|
||||
return m.updatePluginInstalledDetail(msg)
|
||||
case StateAbout:
|
||||
return m.updateAboutView(msg)
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
switch m.state {
|
||||
case StateMainMenu:
|
||||
return m.renderMainMenu()
|
||||
case StateShell:
|
||||
return m.renderShellView()
|
||||
case StatePluginsMenu:
|
||||
return m.renderPluginsMenu()
|
||||
case StatePluginsBrowse:
|
||||
return m.renderPluginsBrowse()
|
||||
case StatePluginDetail:
|
||||
return m.renderPluginDetail()
|
||||
case StatePluginSearch:
|
||||
return m.renderPluginSearch()
|
||||
case StatePluginsInstalled:
|
||||
return m.renderPluginsInstalled()
|
||||
case StatePluginInstalledDetail:
|
||||
return m.renderPluginInstalledDetail()
|
||||
case StateAbout:
|
||||
return m.renderAboutView()
|
||||
default:
|
||||
return m.renderMainMenu()
|
||||
}
|
||||
}
|
||||
167
internal/dms/detector.go
Normal file
167
internal/dms/detector.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/config"
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
"github.com/AvengeMedia/danklinux/internal/distros"
|
||||
)
|
||||
|
||||
type Detector struct {
|
||||
homeDir string
|
||||
distribution distros.Distribution
|
||||
}
|
||||
|
||||
func (d *Detector) GetDistribution() distros.Distribution {
|
||||
return d.distribution
|
||||
}
|
||||
|
||||
func NewDetector() (*Detector, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logChan := make(chan string, 100)
|
||||
go func() {
|
||||
for range logChan {
|
||||
}
|
||||
}()
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dist, err := distros.NewDistribution(osInfo.Distribution.ID, logChan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Detector{
|
||||
homeDir: homeDir,
|
||||
distribution: dist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Detector) IsDMSInstalled() bool {
|
||||
_, err := config.LocateDMSConfig()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetDependencyStatus() ([]deps.Dependency, error) {
|
||||
hyprlandDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerHyprland)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
niriDeps, err := d.distribution.DetectDependencies(context.Background(), deps.WindowManagerNiri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Combine dependencies and deduplicate
|
||||
depMap := make(map[string]deps.Dependency)
|
||||
|
||||
for _, dep := range hyprlandDeps {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
|
||||
for _, dep := range niriDeps {
|
||||
// If dependency already exists, keep the one that's installed or needs update
|
||||
if existing, exists := depMap[dep.Name]; exists {
|
||||
if dep.Status > existing.Status {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
} else {
|
||||
depMap[dep.Name] = dep
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map back to slice
|
||||
var allDeps []deps.Dependency
|
||||
for _, dep := range depMap {
|
||||
allDeps = append(allDeps, dep)
|
||||
}
|
||||
|
||||
return allDeps, nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetWindowManagerStatus() (bool, bool, error) {
|
||||
// Reuse the existing command detection logic from BaseDistribution
|
||||
// Since all distros embed BaseDistribution, we can access it via interface
|
||||
type CommandChecker interface {
|
||||
CommandExists(string) bool
|
||||
}
|
||||
|
||||
checker, ok := d.distribution.(CommandChecker)
|
||||
if !ok {
|
||||
// Fallback to direct command check if interface not available
|
||||
hyprlandInstalled := d.commandExists("hyprland") || d.commandExists("Hyprland")
|
||||
niriInstalled := d.commandExists("niri")
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
hyprlandInstalled := checker.CommandExists("hyprland") || checker.CommandExists("Hyprland")
|
||||
niriInstalled := checker.CommandExists("niri")
|
||||
|
||||
return hyprlandInstalled, niriInstalled, nil
|
||||
}
|
||||
|
||||
func (d *Detector) commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *Detector) GetInstalledComponents() []DependencyInfo {
|
||||
dependencies, err := d.GetDependencyStatus()
|
||||
if err != nil {
|
||||
return []DependencyInfo{}
|
||||
}
|
||||
|
||||
isNixOS := d.isNixOS()
|
||||
|
||||
var components []DependencyInfo
|
||||
for _, dep := range dependencies {
|
||||
// On NixOS, filter out the window managers themselves but keep their components
|
||||
if isNixOS && (dep.Name == "hyprland" || dep.Name == "niri") {
|
||||
continue
|
||||
}
|
||||
|
||||
components = append(components, DependencyInfo{
|
||||
Name: dep.Name,
|
||||
Status: dep.Status,
|
||||
Description: dep.Description,
|
||||
Required: dep.Required,
|
||||
})
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
func (d *Detector) isNixOS() bool {
|
||||
_, err := os.Stat("/etc/nixos")
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// Alternative check
|
||||
if _, err := os.Stat("/nix/store"); err == nil {
|
||||
// Also check for nixos-version command
|
||||
if d.commandExists("nixos-version") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type DependencyInfo struct {
|
||||
Name string
|
||||
Status deps.DependencyStatus
|
||||
Description string
|
||||
Required bool
|
||||
}
|
||||
54
internal/dms/handlers_common.go
Normal file
54
internal/dms/handlers_common.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/log"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateShellView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
default:
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateAboutView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
if msg.String() == "esc" {
|
||||
m.state = StateMainMenu
|
||||
} else {
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func terminateShell() {
|
||||
patterns := []string{"dms run", "qs -c dms"}
|
||||
for _, pattern := range patterns {
|
||||
cmd := exec.Command("pkill", "-f", pattern)
|
||||
cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func startShellDaemon() {
|
||||
cmd := exec.Command("dms", "run", "-d")
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Errorf("Error starting daemon: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func restartShell() {
|
||||
terminateShell()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
startShellDaemon()
|
||||
}
|
||||
391
internal/dms/handlers_features.go
Normal file
391
internal/dms/handlers_features.go
Normal file
@@ -0,0 +1,391 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/deps"
|
||||
"github.com/AvengeMedia/danklinux/internal/distros"
|
||||
"github.com/AvengeMedia/danklinux/internal/greeter"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updateUpdateView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
filteredDeps := m.getFilteredDeps()
|
||||
maxIndex := len(filteredDeps) - 1
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedUpdateDep > 0 {
|
||||
m.selectedUpdateDep--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedUpdateDep < maxIndex {
|
||||
m.selectedUpdateDep++
|
||||
}
|
||||
case " ":
|
||||
if dep := m.getDepAtVisualIndex(m.selectedUpdateDep); dep != nil {
|
||||
m.updateToggles[dep.Name] = !m.updateToggles[dep.Name]
|
||||
}
|
||||
case "enter":
|
||||
hasSelected := false
|
||||
for _, toggle := range m.updateToggles {
|
||||
if toggle {
|
||||
hasSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSelected {
|
||||
m.state = StateMainMenu
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.state = StateUpdatePassword
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateUpdate
|
||||
m.passwordInput = ""
|
||||
m.passwordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.passwordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validatePassword(m.passwordInput)
|
||||
case "backspace":
|
||||
if len(m.passwordInput) > 0 {
|
||||
m.passwordInput = m.passwordInput[:len(m.passwordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.passwordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateProgressView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.updateProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.updateProgress = updateProgressMsg{}
|
||||
m.updateLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) validatePassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return passwordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return passwordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) performUpdate() tea.Cmd {
|
||||
var depsToUpdate []deps.Dependency
|
||||
|
||||
for _, depInfo := range m.updateDeps {
|
||||
if m.updateToggles[depInfo.Name] {
|
||||
depsToUpdate = append(depsToUpdate, deps.Dependency{
|
||||
Name: depInfo.Name,
|
||||
Status: depInfo.Status,
|
||||
Description: depInfo.Description,
|
||||
Required: depInfo.Required,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(depsToUpdate) == 0 {
|
||||
return func() tea.Msg {
|
||||
return updateCompleteMsg{err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
wm := deps.WindowManagerHyprland
|
||||
if m.niriInstalled {
|
||||
wm = deps.WindowManagerNiri
|
||||
}
|
||||
|
||||
sudoPassword := m.sudoPassword
|
||||
reinstallFlags := make(map[string]bool)
|
||||
for name, toggled := range m.updateToggles {
|
||||
if toggled {
|
||||
reinstallFlags[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
distribution := m.detector.GetDistribution()
|
||||
progressChan := m.updateProgressChan
|
||||
|
||||
return func() tea.Msg {
|
||||
installerChan := make(chan distros.InstallProgressMsg, 100)
|
||||
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
err := distribution.InstallPackages(ctx, depsToUpdate, wm, sudoPassword, reinstallFlags, installerChan)
|
||||
close(installerChan)
|
||||
|
||||
if err != nil {
|
||||
progressChan <- updateProgressMsg{complete: true, err: err}
|
||||
} else {
|
||||
progressChan <- updateProgressMsg{complete: true}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for msg := range installerChan {
|
||||
progressChan <- updateProgressMsg{
|
||||
progress: msg.Progress,
|
||||
step: msg.Step,
|
||||
complete: msg.IsComplete,
|
||||
err: msg.Error,
|
||||
logOutput: msg.LogOutput,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedGreeterItem > 0 {
|
||||
m.selectedGreeterItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedGreeterItem < len(greeterMenuItems)-1 {
|
||||
m.selectedGreeterItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedGreeterItem == 0 {
|
||||
compositors := greeter.DetectCompositors()
|
||||
if len(compositors) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
m.greeterCompositors = compositors
|
||||
|
||||
if len(compositors) > 1 {
|
||||
m.state = StateGreeterCompositorSelect
|
||||
m.greeterSelectedComp = 0
|
||||
return m, nil
|
||||
} else {
|
||||
m.greeterChosenCompositor = compositors[0]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterCompositorSelect(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.greeterSelectedComp > 0 {
|
||||
m.greeterSelectedComp--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.greeterSelectedComp < len(m.greeterCompositors)-1 {
|
||||
m.greeterSelectedComp++
|
||||
}
|
||||
case "enter", " ":
|
||||
m.greeterChosenCompositor = m.greeterCompositors[m.greeterSelectedComp]
|
||||
m.state = StateGreeterPassword
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterPasswordView(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateGreeterMenu
|
||||
m.greeterPasswordInput = ""
|
||||
m.greeterPasswordError = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.greeterPasswordInput == "" {
|
||||
return m, nil
|
||||
}
|
||||
return m, m.validateGreeterPassword(m.greeterPasswordInput)
|
||||
case "backspace":
|
||||
if len(m.greeterPasswordInput) > 0 {
|
||||
m.greeterPasswordInput = m.greeterPasswordInput[:len(m.greeterPasswordInput)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 && msg.String()[0] >= 32 && msg.String()[0] <= 126 {
|
||||
m.greeterPasswordInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updateGreeterInstalling(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
if m.greeterProgress.complete {
|
||||
m.state = StateMainMenu
|
||||
m.greeterProgress = greeterProgressMsg{}
|
||||
m.greeterLogs = []string{}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) performGreeterInstall() tea.Cmd {
|
||||
progressChan := m.greeterInstallChan
|
||||
sudoPassword := m.greeterSudoPassword
|
||||
compositor := m.greeterChosenCompositor
|
||||
|
||||
return func() tea.Msg {
|
||||
go func() {
|
||||
logFunc := func(msg string) {
|
||||
progressChan <- greeterProgressMsg{step: msg, logOutput: msg}
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Checking greetd installation..."}
|
||||
if err := performGreeterInstallSteps(progressChan, logFunc, sudoPassword, compositor); err != nil {
|
||||
progressChan <- greeterProgressMsg{step: "Installation failed", complete: true, err: err}
|
||||
return
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Installation complete", complete: true}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) validateGreeterPassword(password string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
fmt.Fprintf(stdin, "%s\n", password)
|
||||
}()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
outputStr := string(output)
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(outputStr, "Sorry, try again") ||
|
||||
strings.Contains(outputStr, "incorrect password") ||
|
||||
strings.Contains(outputStr, "authentication failure") {
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
return greeterPasswordValidMsg{password: "", valid: false}
|
||||
}
|
||||
|
||||
return greeterPasswordValidMsg{password: password, valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
func performGreeterInstallSteps(progressChan chan greeterProgressMsg, logFunc func(string), sudoPassword string, compositor string) error {
|
||||
if err := greeter.EnsureGreetdInstalled(logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Detecting DMS installation..."}
|
||||
dmsPath, err := greeter.DetectDMSPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Found DMS at: %s", dmsPath))
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Selected compositor: %s", compositor))
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Copying greeter files..."}
|
||||
if err := greeter.CopyGreeterFiles(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Configuring greetd..."}
|
||||
if err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
progressChan <- greeterProgressMsg{step: "Synchronizing DMS configurations..."}
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, sudoPassword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
internal/dms/handlers_mainmenu.go
Normal file
61
internal/dms/handlers_mainmenu.go
Normal file
@@ -0,0 +1,61 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateUpdate:
|
||||
m.state = StateUpdate
|
||||
m.selectedUpdateDep = 0
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateGreeterMenu:
|
||||
m.state = StateGreeterMenu
|
||||
m.selectedGreeterItem = 0
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
55
internal/dms/handlers_mainmenu_distro.go
Normal file
55
internal/dms/handlers_mainmenu_distro.go
Normal file
@@ -0,0 +1,55 @@
|
||||
//go:build distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
type shellStartedMsg struct{}
|
||||
|
||||
func (m Model) updateMainMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
return m, tea.Quit
|
||||
case "up", "k":
|
||||
if m.selectedItem > 0 {
|
||||
m.selectedItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedItem < len(m.menuItems)-1 {
|
||||
m.selectedItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedItem < len(m.menuItems) {
|
||||
selectedAction := m.menuItems[m.selectedItem].Action
|
||||
selectedLabel := m.menuItems[m.selectedItem].Label
|
||||
|
||||
switch selectedAction {
|
||||
case StateShell:
|
||||
if selectedLabel == "Terminate Shell" {
|
||||
terminateShell()
|
||||
m.menuItems = m.buildMenuItems()
|
||||
if m.selectedItem >= len(m.menuItems) {
|
||||
m.selectedItem = len(m.menuItems) - 1
|
||||
}
|
||||
} else {
|
||||
startShellDaemon()
|
||||
// Wait a moment for the daemon to actually start before checking status
|
||||
return m, tea.Tick(300*time.Millisecond, func(t time.Time) tea.Msg {
|
||||
return shellStartedMsg{}
|
||||
})
|
||||
}
|
||||
case StatePluginsMenu:
|
||||
m.state = StatePluginsMenu
|
||||
m.selectedPluginsMenuItem = 0
|
||||
m.pluginsMenuItems = m.buildPluginsMenuItems()
|
||||
case StateAbout:
|
||||
m.state = StateAbout
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
339
internal/dms/plugins_handlers.go
Normal file
339
internal/dms/plugins_handlers.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/plugins"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
func (m Model) updatePluginsMenu(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StateMainMenu
|
||||
case "up", "k":
|
||||
if m.selectedPluginsMenuItem > 0 {
|
||||
m.selectedPluginsMenuItem--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems)-1 {
|
||||
m.selectedPluginsMenuItem++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginsMenuItem < len(m.pluginsMenuItems) {
|
||||
selectedAction := m.pluginsMenuItems[m.selectedPluginsMenuItem].Action
|
||||
switch selectedAction {
|
||||
case StatePluginsBrowse:
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginsLoading = true
|
||||
m.pluginsError = ""
|
||||
m.pluginsList = nil
|
||||
return m, loadPlugins
|
||||
case StatePluginsInstalled:
|
||||
m.state = StatePluginsInstalled
|
||||
m.installedPluginsLoading = true
|
||||
m.installedPluginsError = ""
|
||||
m.installedPluginsList = nil
|
||||
return m, loadInstalledPlugins
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "up", "k":
|
||||
if m.selectedPluginIndex > 0 {
|
||||
m.selectedPluginIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList)-1 {
|
||||
m.selectedPluginIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
m.state = StatePluginDetail
|
||||
}
|
||||
case "/":
|
||||
m.state = StatePluginSearch
|
||||
m.pluginSearchQuery = ""
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
case "i":
|
||||
if m.selectedPluginIndex < len(m.filteredPluginsList) {
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if !installed {
|
||||
return m, installPlugin(plugin)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsBrowse
|
||||
m.pluginSearchQuery = ""
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
case "enter":
|
||||
m.state = StatePluginsBrowse
|
||||
m.filterPlugins()
|
||||
case "backspace":
|
||||
if len(m.pluginSearchQuery) > 0 {
|
||||
m.pluginSearchQuery = m.pluginSearchQuery[:len(m.pluginSearchQuery)-1]
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.pluginSearchQuery += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) filterPlugins() {
|
||||
if m.pluginSearchQuery == "" {
|
||||
m.filteredPluginsList = m.pluginsList
|
||||
m.selectedPluginIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
rawPlugins := make([]plugins.Plugin, len(m.pluginsList))
|
||||
for i, p := range m.pluginsList {
|
||||
rawPlugins[i] = plugins.Plugin{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
searchResults := plugins.FuzzySearch(m.pluginSearchQuery, rawPlugins)
|
||||
searchResults = plugins.SortByFirstParty(searchResults)
|
||||
|
||||
filtered := make([]pluginInfo, len(searchResults))
|
||||
for i, p := range searchResults {
|
||||
filtered[i] = pluginInfo{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Author: p.Author,
|
||||
Description: p.Description,
|
||||
Repo: p.Repo,
|
||||
Path: p.Path,
|
||||
Capabilities: p.Capabilities,
|
||||
Compositors: p.Compositors,
|
||||
Dependencies: p.Dependencies,
|
||||
FirstParty: strings.HasPrefix(p.Repo, "https://github.com/AvengeMedia"),
|
||||
}
|
||||
}
|
||||
|
||||
m.filteredPluginsList = filtered
|
||||
m.selectedPluginIndex = 0
|
||||
}
|
||||
|
||||
type pluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
func loadPlugins() tea.Msg {
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
pluginList, err := registry.List()
|
||||
if err != nil {
|
||||
return pluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
return pluginsLoadedMsg{plugins: pluginList}
|
||||
}
|
||||
|
||||
func (m *Model) updatePluginInstallStatus() {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range m.pluginsList {
|
||||
p := plugins.Plugin{ID: plugin.ID}
|
||||
installed, err := manager.IsInstalled(p)
|
||||
if err == nil {
|
||||
m.pluginInstallStatus[plugin.Name] = installed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) updatePluginsInstalled(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsMenu
|
||||
case "up", "k":
|
||||
if m.selectedInstalledIndex > 0 {
|
||||
m.selectedInstalledIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList)-1 {
|
||||
m.selectedInstalledIndex++
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
m.state = StatePluginInstalledDetail
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m Model) updatePluginInstalledDetail(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "esc":
|
||||
m.state = StatePluginsInstalled
|
||||
case "u":
|
||||
if m.selectedInstalledIndex < len(m.installedPluginsList) {
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
return m, uninstallPlugin(plugin)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type installedPluginsLoadedMsg struct {
|
||||
plugins []plugins.Plugin
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginUninstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
type pluginInstalledMsg struct {
|
||||
pluginName string
|
||||
err error
|
||||
}
|
||||
|
||||
func loadInstalledPlugins() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
registry, err := plugins.NewRegistry()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
installedNames, err := manager.ListInstalled()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return installedPluginsLoadedMsg{err: err}
|
||||
}
|
||||
|
||||
var installed []plugins.Plugin
|
||||
for _, id := range installedNames {
|
||||
for _, p := range allPlugins {
|
||||
if p.ID == id {
|
||||
installed = append(installed, p)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installed = plugins.SortByFirstParty(installed)
|
||||
|
||||
return installedPluginsLoadedMsg{plugins: installed}
|
||||
}
|
||||
|
||||
func installPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Install(p); err != nil {
|
||||
return pluginInstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginInstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
|
||||
func uninstallPlugin(plugin pluginInfo) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manager, err := plugins.NewManager()
|
||||
if err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
p := plugins.Plugin{
|
||||
ID: plugin.ID,
|
||||
Name: plugin.Name,
|
||||
Category: plugin.Category,
|
||||
Author: plugin.Author,
|
||||
Description: plugin.Description,
|
||||
Repo: plugin.Repo,
|
||||
Path: plugin.Path,
|
||||
Capabilities: plugin.Capabilities,
|
||||
Compositors: plugin.Compositors,
|
||||
Dependencies: plugin.Dependencies,
|
||||
}
|
||||
|
||||
if err := manager.Uninstall(p); err != nil {
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name, err: err}
|
||||
}
|
||||
|
||||
return pluginUninstalledMsg{pluginName: plugin.Name}
|
||||
}
|
||||
}
|
||||
367
internal/dms/plugins_views.go
Normal file
367
internal/dms/plugins_views.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderPluginsMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
for i, item := range m.pluginsMenuItems {
|
||||
if i == m.selectedPluginsMenuItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Select | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsBrowse() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Browse Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.pluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Fetching plugins from registry..."))
|
||||
} else if m.pluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.pluginsError)))
|
||||
} else if len(m.filteredPluginsList) == 0 {
|
||||
if m.pluginSearchQuery != "" {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("No plugins match '%s'", m.pluginSearchQuery)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render("No plugins found in registry."))
|
||||
}
|
||||
} else {
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
for i, plugin := range m.filteredPluginsList {
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
installMarker := ""
|
||||
if installed {
|
||||
installMarker = " [Installed]"
|
||||
}
|
||||
|
||||
if i == m.selectedPluginIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
if installed {
|
||||
b.WriteString(installedStyle.Render(installMarker))
|
||||
}
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.pluginsLoading || m.pluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: View/Install | /: Search | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.selectedPluginIndex >= len(m.filteredPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.filteredPluginsList[m.selectedPluginIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
installed := m.pluginInstallStatus[plugin.Name]
|
||||
if installed {
|
||||
b.WriteString(labelStyle.Render("Status: "))
|
||||
installedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
b.WriteString(installedStyle.Render("Installed"))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if installed {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("i: Install | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginSearch() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(titleStyle.Render("Search Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Query: "))
|
||||
b.WriteString(titleStyle.Render(m.pluginSearchQuery + "▌"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Enter: Search | Esc: Cancel"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginsInstalled() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
b.WriteString(titleStyle.Render("Installed Plugins"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if m.installedPluginsLoading {
|
||||
b.WriteString(normalStyle.Render("Loading installed plugins..."))
|
||||
} else if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
} else if len(m.installedPluginsList) == 0 {
|
||||
b.WriteString(normalStyle.Render("No plugins installed."))
|
||||
} else {
|
||||
for i, plugin := range m.installedPluginsList {
|
||||
if i == m.selectedInstalledIndex {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("→ %s", plugin.Name)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", plugin.Name)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
if m.installedPluginsLoading || m.installedPluginsError != "" {
|
||||
b.WriteString(instructionStyle.Render("Esc: Back | q: Quit"))
|
||||
} else {
|
||||
b.WriteString(instructionStyle.Render("↑/↓: Navigate | Enter: Details | Esc: Back | q: Quit"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPluginInstalledDetail() string {
|
||||
var b strings.Builder
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
labelStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
if m.selectedInstalledIndex >= len(m.installedPluginsList) {
|
||||
return "No plugin selected"
|
||||
}
|
||||
|
||||
plugin := m.installedPluginsList[m.selectedInstalledIndex]
|
||||
|
||||
b.WriteString(titleStyle.Render(plugin.Name))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("ID: "))
|
||||
b.WriteString(normalStyle.Render(plugin.ID))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Category: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Category))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Author: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Author))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Description:"))
|
||||
b.WriteString("\n")
|
||||
wrapped := wrapText(plugin.Description, 60)
|
||||
b.WriteString(normalStyle.Render(wrapped))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(labelStyle.Render("Repository: "))
|
||||
b.WriteString(normalStyle.Render(plugin.Repo))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(plugin.Capabilities) > 0 {
|
||||
b.WriteString(labelStyle.Render("Capabilities: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Capabilities, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Compositors) > 0 {
|
||||
b.WriteString(labelStyle.Render("Compositors: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Compositors, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if len(plugin.Dependencies) > 0 {
|
||||
b.WriteString(labelStyle.Render("Dependencies: "))
|
||||
b.WriteString(normalStyle.Render(strings.Join(plugin.Dependencies, ", ")))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
if m.installedPluginsError != "" {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Error: %s", m.installedPluginsError)))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("u: Uninstall | Esc: Back | q: Quit"))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func wrapText(text string, width int) string {
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
var lines []string
|
||||
currentLine := words[0]
|
||||
|
||||
for _, word := range words[1:] {
|
||||
if len(currentLine)+1+len(word) <= width {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
lines = append(lines, currentLine)
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
149
internal/dms/views_common.go
Normal file
149
internal/dms/views_common.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/tui"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderMainMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("dms"))
|
||||
b.WriteString("\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range m.menuItems {
|
||||
if i == m.selectedItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item.Label)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item.Label)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, q/Esc: Exit"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderShellView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Shell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Opening interactive shell..."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("This will launch a shell with DMS environment loaded."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Press any key to launch shell, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderAboutView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("About DankMaterialShell"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf("DMS Management Interface %s", m.version)))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("DankMaterialShell is a comprehensive desktop environment"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("built around Quickshell, providing a modern Material Design"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("experience for Wayland compositors."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(normalStyle.Render("Components:"))
|
||||
b.WriteString("\n")
|
||||
for _, dep := range m.dependencies {
|
||||
status := "✗"
|
||||
if dep.Status == 1 {
|
||||
status = "✓"
|
||||
}
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s %s", status, dep.Name)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Esc: Back to main menu"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderBanner() string {
|
||||
theme := tui.TerminalTheme()
|
||||
|
||||
logo := `
|
||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
||||
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(theme.Primary)).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
return titleStyle.Render(logo)
|
||||
}
|
||||
529
internal/dms/views_features.go
Normal file
529
internal/dms/views_features.go
Normal file
@@ -0,0 +1,529 @@
|
||||
//go:build !distro_binary
|
||||
|
||||
package dms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) renderUpdateView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Update Dependencies"))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateDeps) == 0 {
|
||||
b.WriteString("Loading dependencies...\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
categories := m.categorizeDependencies()
|
||||
currentIndex := 0
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if !exists || len(deps) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
categoryStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7060ac")).
|
||||
Bold(true).
|
||||
MarginTop(1)
|
||||
|
||||
b.WriteString(categoryStyle.Render(category + ":"))
|
||||
b.WriteString("\n")
|
||||
|
||||
for _, dep := range deps {
|
||||
var statusText, icon, reinstallMarker string
|
||||
var style lipgloss.Style
|
||||
|
||||
if m.updateToggles[dep.Name] {
|
||||
reinstallMarker = "🔄 "
|
||||
if dep.Status == 0 {
|
||||
statusText = "Will be installed"
|
||||
} else {
|
||||
statusText = "Will be upgraded"
|
||||
}
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
} else {
|
||||
switch dep.Status {
|
||||
case 1:
|
||||
icon = "✓"
|
||||
statusText = "Installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
|
||||
case 0:
|
||||
icon = "○"
|
||||
statusText = "Not installed"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
case 2:
|
||||
icon = "△"
|
||||
statusText = "Needs update"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
case 3:
|
||||
icon = "!"
|
||||
statusText = "Needs reinstall"
|
||||
style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500"))
|
||||
}
|
||||
}
|
||||
|
||||
line := fmt.Sprintf("%s%s%-25s %s", reinstallMarker, icon, dep.Name, statusText)
|
||||
|
||||
if currentIndex == m.selectedUpdateDep {
|
||||
line = "▶ " + line
|
||||
selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#7060ac")).Bold(true)
|
||||
b.WriteString(selectedStyle.Render(line))
|
||||
} else {
|
||||
line = " " + line
|
||||
b.WriteString(style.Render(line))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Space: Toggle, Enter: Update Selected, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Package installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.passwordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.passwordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.passwordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderProgressView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Updating Packages"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.updateProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.updateProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
progressBar := fmt.Sprintf("[%s%s] %.0f%%",
|
||||
strings.Repeat("█", int(m.updateProgress.progress*30)),
|
||||
strings.Repeat("░", 30-int(m.updateProgress.progress*30)),
|
||||
m.updateProgress.progress*100)
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Render(progressBar))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Live Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 8
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.updateProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Update failed: %v", m.updateProgress.err)))
|
||||
b.WriteString("\n")
|
||||
|
||||
if len(m.updateLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Error Logs:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 15
|
||||
startIdx := 0
|
||||
if len(m.updateLogs) > maxLines {
|
||||
startIdx = len(m.updateLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.updateLogs); i++ {
|
||||
if m.updateLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.updateLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.updateProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Update complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) getFilteredDeps() []DependencyInfo {
|
||||
categories := m.categorizeDependencies()
|
||||
var filtered []DependencyInfo
|
||||
|
||||
for _, category := range []string{"Shell", "Shared Components", "Hyprland Components", "Niri Components"} {
|
||||
deps, exists := categories[category]
|
||||
if exists {
|
||||
filtered = append(filtered, deps...)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (m Model) getDepAtVisualIndex(index int) *DependencyInfo {
|
||||
filtered := m.getFilteredDeps()
|
||||
if index >= 0 && index < len(filtered) {
|
||||
return &filtered[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterPasswordView() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Sudo Authentication"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Greeter installation requires sudo privileges."))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render("Please enter your password to continue:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
inputStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
maskedPassword := strings.Repeat("*", len(m.greeterPasswordInput))
|
||||
b.WriteString(inputStyle.Render("Password: " + maskedPassword))
|
||||
b.WriteString("\n")
|
||||
|
||||
if m.greeterPasswordError != "" {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
b.WriteString(errorStyle.Render("✗ " + m.greeterPasswordError))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "Enter: Continue, Esc: Back, Ctrl+C: Cancel"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterCompositorSelect() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Select Compositor"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("Multiple compositors detected. Choose which one to use for the greeter:"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
for i, comp := range m.greeterCompositors {
|
||||
if i == m.greeterSelectedComp {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", comp)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", comp)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterMenu() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Greeter Management"))
|
||||
b.WriteString("\n")
|
||||
|
||||
greeterMenuItems := []string{"Install Greeter"}
|
||||
|
||||
selectedStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA")).
|
||||
Bold(true)
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
for i, item := range greeterMenuItems {
|
||||
if i == m.selectedGreeterItem {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("▶ %s", item)))
|
||||
} else {
|
||||
b.WriteString(normalStyle.Render(fmt.Sprintf(" %s", item)))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888")).
|
||||
MarginTop(1)
|
||||
|
||||
instructions := "↑/↓: Navigate, Enter: Select, Esc: Back"
|
||||
b.WriteString(instructionStyle.Render(instructions))
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) renderGreeterInstalling() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(m.renderBanner())
|
||||
b.WriteString("\n")
|
||||
|
||||
headerStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF")).
|
||||
Bold(true).
|
||||
MarginBottom(1)
|
||||
|
||||
b.WriteString(headerStyle.Render("Installing Greeter"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if !m.greeterProgress.complete {
|
||||
progressStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString(progressStyle.Render(m.greeterProgress.step))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if len(m.greeterLogs) > 0 {
|
||||
b.WriteString("\n")
|
||||
logHeader := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Render("Output:")
|
||||
b.WriteString(logHeader)
|
||||
b.WriteString("\n")
|
||||
|
||||
maxLines := 10
|
||||
startIdx := 0
|
||||
if len(m.greeterLogs) > maxLines {
|
||||
startIdx = len(m.greeterLogs) - maxLines
|
||||
}
|
||||
|
||||
logStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
|
||||
for i := startIdx; i < len(m.greeterLogs); i++ {
|
||||
if m.greeterLogs[i] != "" {
|
||||
b.WriteString(logStyle.Render(" " + m.greeterLogs[i]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.greeterProgress.err != nil {
|
||||
errorStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Installation failed: %v", m.greeterProgress.err)))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to go back"))
|
||||
} else if m.greeterProgress.complete {
|
||||
successStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00D4AA"))
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(successStyle.Render("✓ Greeter installation complete!"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
normalStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FFFFFF"))
|
||||
|
||||
b.WriteString(normalStyle.Render("To test the greeter, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl start greetd"))
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(normalStyle.Render("To enable on boot, run:"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(normalStyle.Render(" sudo systemctl enable --now greetd"))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
instructionStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#888888"))
|
||||
b.WriteString(instructionStyle.Render("Press Esc to return to main menu"))
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m Model) categorizeDependencies() map[string][]DependencyInfo {
|
||||
categories := map[string][]DependencyInfo{
|
||||
"Shell": {},
|
||||
"Shared Components": {},
|
||||
"Hyprland Components": {},
|
||||
"Niri Components": {},
|
||||
}
|
||||
|
||||
excludeList := map[string]bool{
|
||||
"git": true,
|
||||
"polkit-agent": true,
|
||||
"jq": true,
|
||||
"xdg-desktop-portal": true,
|
||||
"xdg-desktop-portal-wlr": true,
|
||||
"xdg-desktop-portal-hyprland": true,
|
||||
"xdg-desktop-portal-gtk": true,
|
||||
}
|
||||
|
||||
for _, dep := range m.updateDeps {
|
||||
if excludeList[dep.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
switch dep.Name {
|
||||
case "dms (DankMaterialShell)", "quickshell":
|
||||
categories["Shell"] = append(categories["Shell"], dep)
|
||||
case "hyprland", "grim", "slurp", "hyprctl", "grimblast":
|
||||
categories["Hyprland Components"] = append(categories["Hyprland Components"], dep)
|
||||
case "niri":
|
||||
categories["Niri Components"] = append(categories["Niri Components"], dep)
|
||||
case "kitty", "alacritty", "ghostty", "hyprpicker":
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
default:
|
||||
categories["Shared Components"] = append(categories["Shared Components"], dep)
|
||||
}
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
65
internal/errdefs/errdefs.go
Normal file
65
internal/errdefs/errdefs.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package errdefs
|
||||
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
ErrTypeNotLinux ErrorType = iota
|
||||
ErrTypeInvalidArchitecture
|
||||
ErrTypeUnsupportedDistribution
|
||||
ErrTypeUnsupportedVersion
|
||||
ErrTypeUpdateCancelled
|
||||
ErrTypeNoUpdateNeeded
|
||||
ErrTypeInvalidTemperature
|
||||
ErrTypeInvalidGamma
|
||||
ErrTypeInvalidLocation
|
||||
ErrTypeInvalidManualTimes
|
||||
ErrTypeNoWaylandDisplay
|
||||
ErrTypeNoGammaControl
|
||||
ErrTypeNotInitialized
|
||||
ErrTypeSecretPromptCancelled
|
||||
ErrTypeSecretPromptTimeout
|
||||
ErrTypeSecretAgentFailed
|
||||
ErrTypeGeneric
|
||||
)
|
||||
|
||||
type CustomError struct {
|
||||
Type ErrorType
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *CustomError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewCustomError(errType ErrorType, message string) error {
|
||||
return &CustomError{
|
||||
Type: errType,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ErrBadCredentials = "bad-credentials"
|
||||
ErrNoSuchSSID = "no-such-ssid"
|
||||
ErrAssocTimeout = "assoc-timeout"
|
||||
ErrDhcpTimeout = "dhcp-timeout"
|
||||
ErrUserCanceled = "user-canceled"
|
||||
ErrWifiDisabled = "wifi-disabled"
|
||||
ErrAlreadyConnected = "already-connected"
|
||||
ErrConnectionFailed = "connection-failed"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUpdateCancelled = NewCustomError(ErrTypeUpdateCancelled, "update cancelled by user")
|
||||
ErrNoUpdateNeeded = NewCustomError(ErrTypeNoUpdateNeeded, "no update needed")
|
||||
ErrInvalidTemperature = NewCustomError(ErrTypeInvalidTemperature, "temperature must be between 1000 and 10000")
|
||||
ErrInvalidGamma = NewCustomError(ErrTypeInvalidGamma, "gamma must be between 0 and 10")
|
||||
ErrInvalidLocation = NewCustomError(ErrTypeInvalidLocation, "invalid latitude/longitude")
|
||||
ErrInvalidManualTimes = NewCustomError(ErrTypeInvalidManualTimes, "both sunrise and sunset must be set or neither")
|
||||
ErrNoWaylandDisplay = NewCustomError(ErrTypeNoWaylandDisplay, "no wayland display available")
|
||||
ErrNoGammaControl = NewCustomError(ErrTypeNoGammaControl, "compositor does not support gamma control")
|
||||
ErrNotInitialized = NewCustomError(ErrTypeNotInitialized, "manager not initialized")
|
||||
ErrSecretPromptCancelled = NewCustomError(ErrTypeSecretPromptCancelled, "secret prompt cancelled by user")
|
||||
ErrSecretPromptTimeout = NewCustomError(ErrTypeSecretPromptTimeout, "secret prompt timed out")
|
||||
ErrSecretAgentFailed = NewCustomError(ErrTypeSecretAgentFailed, "secret agent operation failed")
|
||||
)
|
||||
490
internal/greeter/installer.go
Normal file
490
internal/greeter/installer.go
Normal file
@@ -0,0 +1,490 @@
|
||||
package greeter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/config"
|
||||
"github.com/AvengeMedia/danklinux/internal/distros"
|
||||
)
|
||||
|
||||
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
|
||||
func DetectDMSPath() (string, error) {
|
||||
return config.LocateDMSConfig()
|
||||
}
|
||||
|
||||
// DetectCompositors checks which compositors are installed
|
||||
func DetectCompositors() []string {
|
||||
var compositors []string
|
||||
|
||||
if commandExists("niri") {
|
||||
compositors = append(compositors, "niri")
|
||||
}
|
||||
if commandExists("Hyprland") {
|
||||
compositors = append(compositors, "Hyprland")
|
||||
}
|
||||
|
||||
return compositors
|
||||
}
|
||||
|
||||
// PromptCompositorChoice asks user to choose between compositors
|
||||
func PromptCompositorChoice(compositors []string) (string, error) {
|
||||
fmt.Println("\nMultiple compositors detected:")
|
||||
for i, comp := range compositors {
|
||||
fmt.Printf("%d) %s\n", i+1, comp)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Choose compositor for greeter (1-2): ")
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
|
||||
response = strings.TrimSpace(response)
|
||||
switch response {
|
||||
case "1":
|
||||
return compositors[0], nil
|
||||
case "2":
|
||||
if len(compositors) > 1 {
|
||||
return compositors[1], nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid choice")
|
||||
default:
|
||||
return "", fmt.Errorf("invalid choice")
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureGreetdInstalled checks if greetd is installed and installs it if not
|
||||
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
||||
if commandExists("greetd") {
|
||||
logFunc("✓ greetd is already installed")
|
||||
return nil
|
||||
}
|
||||
|
||||
logFunc("greetd is not installed. Installing...")
|
||||
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to detect OS: %w", err)
|
||||
}
|
||||
|
||||
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||
if !exists {
|
||||
return fmt.Errorf("unsupported distribution for automatic greetd installation: %s", osInfo.Distribution.ID)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var installCmd *exec.Cmd
|
||||
|
||||
switch config.Family {
|
||||
case distros.FamilyArch:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"pacman -S --needed --noconfirm greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyFedora:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"dnf install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilySUSE:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"zypper install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyUbuntu:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyDebian:
|
||||
if sudoPassword != "" {
|
||||
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
||||
"apt-get install -y greetd")
|
||||
} else {
|
||||
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
||||
}
|
||||
|
||||
case distros.FamilyNix:
|
||||
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
||||
}
|
||||
|
||||
installCmd.Stdout = os.Stdout
|
||||
installCmd.Stderr = os.Stderr
|
||||
|
||||
if err := installCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to install greetd: %w", err)
|
||||
}
|
||||
|
||||
logFunc("✓ greetd installed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
// Check if dms-greeter is already in PATH
|
||||
if commandExists("dms-greeter") {
|
||||
logFunc("✓ dms-greeter wrapper already installed")
|
||||
} else {
|
||||
// Install the wrapper script
|
||||
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
|
||||
wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
|
||||
|
||||
if _, err := os.Stat(wrapperSrc); os.IsNotExist(err) {
|
||||
return fmt.Errorf("dms-greeter wrapper not found at %s", wrapperSrc)
|
||||
}
|
||||
|
||||
wrapperDst := "/usr/local/bin/dms-greeter"
|
||||
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Installed dms-greeter wrapper to %s", wrapperDst))
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
||||
}
|
||||
|
||||
// Set SELinux context on Fedora and openSUSE
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err == nil {
|
||||
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
|
||||
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
||||
} else {
|
||||
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
||||
} else {
|
||||
logFunc("✓ Restored SELinux context for dms-greeter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create cache directory with proper permissions
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 750)", cacheDir))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
|
||||
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
if !commandExists("setfacl") {
|
||||
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
|
||||
logFunc(" If theme sync doesn't work, you may need to install acl package:")
|
||||
logFunc(" - Fedora/RHEL: sudo dnf install acl")
|
||||
logFunc(" - Debian/Ubuntu: sudo apt-get install acl")
|
||||
logFunc(" - Arch: sudo pacman -S acl")
|
||||
return nil
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
parentDirs := []struct {
|
||||
path string
|
||||
desc string
|
||||
}{
|
||||
{homeDir, "home directory"},
|
||||
{filepath.Join(homeDir, ".config"), ".config directory"},
|
||||
{filepath.Join(homeDir, ".local"), ".local directory"},
|
||||
{filepath.Join(homeDir, ".cache"), ".cache directory"},
|
||||
{filepath.Join(homeDir, ".local", "state"), ".local/state directory"},
|
||||
}
|
||||
|
||||
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
||||
|
||||
for _, dir := range parentDirs {
|
||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Set ACL to allow greeter user execute (traverse) permission
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:x", dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
|
||||
continue
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Set ACL on %s", dir.desc))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
currentUser := os.Getenv("USER")
|
||||
if currentUser == "" {
|
||||
currentUser = os.Getenv("LOGNAME")
|
||||
}
|
||||
if currentUser == "" {
|
||||
return fmt.Errorf("failed to determine current user")
|
||||
}
|
||||
|
||||
// Check if user is already in greeter group
|
||||
groupsCmd := exec.Command("groups", currentUser)
|
||||
groupsOutput, err := groupsCmd.Output()
|
||||
if err == nil && strings.Contains(string(groupsOutput), "greeter") {
|
||||
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser))
|
||||
} else {
|
||||
// Add current user to greeter group for file access permissions
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil {
|
||||
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser))
|
||||
}
|
||||
|
||||
configDirs := []struct {
|
||||
path string
|
||||
desc string
|
||||
}{
|
||||
{filepath.Join(homeDir, ".config", "DankMaterialShell"), "DankMaterialShell config"},
|
||||
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
|
||||
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
|
||||
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
||||
}
|
||||
|
||||
for _, dir := range configDirs {
|
||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Set group permissions for %s", dir.desc))
|
||||
}
|
||||
|
||||
// Set up ACLs on parent directories to allow greeter user traversal
|
||||
if err := SetupParentDirectoryACLs(logFunc, sudoPassword); err != nil {
|
||||
return fmt.Errorf("failed to setup parent directory ACLs: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
|
||||
symlinks := []struct {
|
||||
source string
|
||||
target string
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"),
|
||||
target: filepath.Join(cacheDir, "settings.json"),
|
||||
desc: "core settings (theme, clock formats, etc)",
|
||||
},
|
||||
{
|
||||
source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"),
|
||||
target: filepath.Join(cacheDir, "session.json"),
|
||||
desc: "state (wallpaper configuration)",
|
||||
},
|
||||
{
|
||||
source: filepath.Join(homeDir, ".cache", "quickshell", "dankshell", "dms-colors.json"),
|
||||
target: filepath.Join(cacheDir, "colors.json"),
|
||||
desc: "wallpaper based theming",
|
||||
},
|
||||
}
|
||||
|
||||
for _, link := range symlinks {
|
||||
sourceDir := filepath.Dir(link.source)
|
||||
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(link.source, []byte("{}"), 0644); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))
|
||||
continue
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
backupPath := configPath + ".backup"
|
||||
if err := runSudoCmd(sudoPassword, "cp", configPath, backupPath); err != nil {
|
||||
return fmt.Errorf("failed to backup config: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
||||
}
|
||||
|
||||
var configContent string
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
configContent = string(data)
|
||||
} else {
|
||||
configContent = `[terminal]
|
||||
vt = 1
|
||||
|
||||
[default_session]
|
||||
|
||||
user = "greeter"
|
||||
`
|
||||
}
|
||||
|
||||
lines := strings.Split(configContent, "\n")
|
||||
var newLines []string
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||
newLines = append(newLines, `user = "greeter"`)
|
||||
} else {
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine wrapper command path
|
||||
wrapperCmd := "dms-greeter"
|
||||
if !commandExists("dms-greeter") {
|
||||
wrapperCmd = "/usr/local/bin/dms-greeter"
|
||||
}
|
||||
|
||||
// Build command based on compositor and dms path
|
||||
compositorLower := strings.ToLower(compositor)
|
||||
command := fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
|
||||
|
||||
var finalLines []string
|
||||
inDefaultSession := false
|
||||
commandAdded := false
|
||||
|
||||
for _, line := range newLines {
|
||||
finalLines = append(finalLines, line)
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if trimmed == "[default_session]" {
|
||||
inDefaultSession = true
|
||||
}
|
||||
|
||||
if inDefaultSession && !commandAdded && trimmed != "" && !strings.HasPrefix(trimmed, "[") {
|
||||
if !strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "user") {
|
||||
finalLines = append(finalLines, command)
|
||||
commandAdded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !commandAdded {
|
||||
finalLines = append(finalLines, command)
|
||||
}
|
||||
|
||||
newConfig := strings.Join(finalLines, "\n")
|
||||
|
||||
tmpFile := "/tmp/greetd-config.toml"
|
||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write temp config: %w", err)
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "mv", tmpFile, configPath); err != nil {
|
||||
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
if sudoPassword != "" {
|
||||
fullArgs := append([]string{command}, args...)
|
||||
quotedArgs := make([]string, len(fullArgs))
|
||||
for i, arg := range fullArgs {
|
||||
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
||||
}
|
||||
cmdStr := strings.Join(quotedArgs, " ")
|
||||
|
||||
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
||||
} else {
|
||||
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
331
internal/hyprland/keybinds.go
Normal file
331
internal/hyprland/keybinds.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package hyprland
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TitleRegex = "#+!"
|
||||
HideComment = "[hidden]"
|
||||
CommentBindPattern = "#/#"
|
||||
)
|
||||
|
||||
var ModSeparators = []rune{'+', ' '}
|
||||
|
||||
type KeyBinding struct {
|
||||
Mods []string `json:"mods"`
|
||||
Key string `json:"key"`
|
||||
Dispatcher string `json:"dispatcher"`
|
||||
Params string `json:"params"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type Section struct {
|
||||
Children []Section `json:"children"`
|
||||
Keybinds []KeyBinding `json:"keybinds"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
contentLines []string
|
||||
readingLine int
|
||||
}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
contentLines: []string{},
|
||||
readingLine: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ReadContent(directory string) error {
|
||||
expandedDir := os.ExpandEnv(directory)
|
||||
expandedDir = filepath.Clean(expandedDir)
|
||||
if strings.HasPrefix(expandedDir, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedDir = filepath.Join(home, expandedDir[1:])
|
||||
}
|
||||
|
||||
info, err := os.Stat(expandedDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
confFiles, err := filepath.Glob(filepath.Join(expandedDir, "*.conf"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(confFiles) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
var combinedContent []string
|
||||
for _, confFile := range confFiles {
|
||||
if fileInfo, err := os.Stat(confFile); err == nil && fileInfo.Mode().IsRegular() {
|
||||
data, err := os.ReadFile(confFile)
|
||||
if err == nil {
|
||||
combinedContent = append(combinedContent, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(combinedContent) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fullContent := strings.Join(combinedContent, "\n")
|
||||
p.contentLines = strings.Split(fullContent, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func autogenerateComment(dispatcher, params string) string {
|
||||
switch dispatcher {
|
||||
case "resizewindow":
|
||||
return "Resize window"
|
||||
|
||||
case "movewindow":
|
||||
if params == "" {
|
||||
return "Move window"
|
||||
}
|
||||
dirMap := map[string]string{
|
||||
"l": "left",
|
||||
"r": "right",
|
||||
"u": "up",
|
||||
"d": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "move in " + dir + " direction"
|
||||
}
|
||||
return "move in null direction"
|
||||
|
||||
case "pin":
|
||||
return "pin (show on all workspaces)"
|
||||
|
||||
case "splitratio":
|
||||
return "Window split ratio " + params
|
||||
|
||||
case "togglefloating":
|
||||
return "Float/unfloat window"
|
||||
|
||||
case "resizeactive":
|
||||
return "Resize window by " + params
|
||||
|
||||
case "killactive":
|
||||
return "Close window"
|
||||
|
||||
case "fullscreen":
|
||||
fsMap := map[string]string{
|
||||
"0": "fullscreen",
|
||||
"1": "maximization",
|
||||
"2": "fullscreen on Hyprland's side",
|
||||
}
|
||||
if fs, ok := fsMap[params]; ok {
|
||||
return "Toggle " + fs
|
||||
}
|
||||
return "Toggle null"
|
||||
|
||||
case "fakefullscreen":
|
||||
return "Toggle fake fullscreen"
|
||||
|
||||
case "workspace":
|
||||
switch params {
|
||||
case "+1":
|
||||
return "focus right"
|
||||
case "-1":
|
||||
return "focus left"
|
||||
}
|
||||
return "focus workspace " + params
|
||||
case "movefocus":
|
||||
dirMap := map[string]string{
|
||||
"l": "left",
|
||||
"r": "right",
|
||||
"u": "up",
|
||||
"d": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "move focus " + dir
|
||||
}
|
||||
return "move focus null"
|
||||
|
||||
case "swapwindow":
|
||||
dirMap := map[string]string{
|
||||
"l": "left",
|
||||
"r": "right",
|
||||
"u": "up",
|
||||
"d": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "swap in " + dir + " direction"
|
||||
}
|
||||
return "swap in null direction"
|
||||
|
||||
case "movetoworkspace":
|
||||
switch params {
|
||||
case "+1":
|
||||
return "move to right workspace (non-silent)"
|
||||
case "-1":
|
||||
return "move to left workspace (non-silent)"
|
||||
}
|
||||
return "move to workspace " + params + " (non-silent)"
|
||||
case "movetoworkspacesilent":
|
||||
switch params {
|
||||
case "+1":
|
||||
return "move to right workspace"
|
||||
case "-1":
|
||||
return "move to right workspace"
|
||||
}
|
||||
return "move to workspace " + params
|
||||
|
||||
case "togglespecialworkspace":
|
||||
return "toggle special"
|
||||
|
||||
case "exec":
|
||||
return params
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
|
||||
line := p.contentLines[lineNumber]
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := parts[1]
|
||||
keyParts := strings.SplitN(keys, "#", 2)
|
||||
keys = keyParts[0]
|
||||
|
||||
var comment string
|
||||
if len(keyParts) > 1 {
|
||||
comment = strings.TrimSpace(keyParts[1])
|
||||
}
|
||||
|
||||
keyFields := strings.SplitN(keys, ",", 5)
|
||||
if len(keyFields) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(keyFields[0])
|
||||
key := strings.TrimSpace(keyFields[1])
|
||||
dispatcher := strings.TrimSpace(keyFields[2])
|
||||
|
||||
var params string
|
||||
if len(keyFields) > 3 {
|
||||
paramParts := keyFields[3:]
|
||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||
}
|
||||
|
||||
if comment != "" {
|
||||
if strings.HasPrefix(comment, HideComment) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
comment = autogenerateComment(dispatcher, params)
|
||||
}
|
||||
|
||||
var modList []string
|
||||
if mods != "" {
|
||||
modstring := mods + string(ModSeparators[0])
|
||||
p := 0
|
||||
for index, char := range modstring {
|
||||
isModSep := false
|
||||
for _, sep := range ModSeparators {
|
||||
if char == sep {
|
||||
isModSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isModSep {
|
||||
if index-p > 1 {
|
||||
modList = append(modList, modstring[p:index])
|
||||
}
|
||||
p = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &KeyBinding{
|
||||
Mods: modList,
|
||||
Key: key,
|
||||
Dispatcher: dispatcher,
|
||||
Params: params,
|
||||
Comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section {
|
||||
titleRegex := regexp.MustCompile(TitleRegex)
|
||||
|
||||
for p.readingLine < len(p.contentLines) {
|
||||
line := p.contentLines[p.readingLine]
|
||||
|
||||
loc := titleRegex.FindStringIndex(line)
|
||||
if loc != nil && loc[0] == 0 {
|
||||
headingScope := strings.Index(line, "!")
|
||||
|
||||
if headingScope <= scope {
|
||||
p.readingLine--
|
||||
return currentContent
|
||||
}
|
||||
|
||||
sectionName := strings.TrimSpace(line[headingScope+1:])
|
||||
p.readingLine++
|
||||
|
||||
childSection := &Section{
|
||||
Children: []Section{},
|
||||
Keybinds: []KeyBinding{},
|
||||
Name: sectionName,
|
||||
}
|
||||
result := p.getBindsRecursive(childSection, headingScope)
|
||||
currentContent.Children = append(currentContent.Children, *result)
|
||||
|
||||
} else if strings.HasPrefix(line, CommentBindPattern) {
|
||||
keybind := p.getKeybindAtLine(p.readingLine)
|
||||
if keybind != nil {
|
||||
currentContent.Keybinds = append(currentContent.Keybinds, *keybind)
|
||||
}
|
||||
|
||||
} else if line == "" || !strings.HasPrefix(strings.TrimSpace(line), "bind") {
|
||||
|
||||
} else {
|
||||
keybind := p.getKeybindAtLine(p.readingLine)
|
||||
if keybind != nil {
|
||||
currentContent.Keybinds = append(currentContent.Keybinds, *keybind)
|
||||
}
|
||||
}
|
||||
|
||||
p.readingLine++
|
||||
}
|
||||
|
||||
return currentContent
|
||||
}
|
||||
|
||||
func (p *Parser) ParseKeys() *Section {
|
||||
p.readingLine = 0
|
||||
rootSection := &Section{
|
||||
Children: []Section{},
|
||||
Keybinds: []KeyBinding{},
|
||||
Name: "",
|
||||
}
|
||||
return p.getBindsRecursive(rootSection, 0)
|
||||
}
|
||||
|
||||
func ParseKeys(path string) (*Section, error) {
|
||||
parser := NewParser()
|
||||
if err := parser.ReadContent(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parser.ParseKeys(), nil
|
||||
}
|
||||
396
internal/hyprland/keybinds_test.go
Normal file
396
internal/hyprland/keybinds_test.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package hyprland
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAutogenerateComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
dispatcher string
|
||||
params string
|
||||
expected string
|
||||
}{
|
||||
{"resizewindow", "", "Resize window"},
|
||||
{"movewindow", "", "Move window"},
|
||||
{"movewindow", "l", "move in left direction"},
|
||||
{"movewindow", "r", "move in right direction"},
|
||||
{"movewindow", "u", "move in up direction"},
|
||||
{"movewindow", "d", "move in down direction"},
|
||||
{"pin", "", "pin (show on all workspaces)"},
|
||||
{"splitratio", "0.5", "Window split ratio 0.5"},
|
||||
{"togglefloating", "", "Float/unfloat window"},
|
||||
{"resizeactive", "10 20", "Resize window by 10 20"},
|
||||
{"killactive", "", "Close window"},
|
||||
{"fullscreen", "0", "Toggle fullscreen"},
|
||||
{"fullscreen", "1", "Toggle maximization"},
|
||||
{"fullscreen", "2", "Toggle fullscreen on Hyprland's side"},
|
||||
{"fakefullscreen", "", "Toggle fake fullscreen"},
|
||||
{"workspace", "+1", "focus right"},
|
||||
{"workspace", "-1", "focus left"},
|
||||
{"workspace", "5", "focus workspace 5"},
|
||||
{"movefocus", "l", "move focus left"},
|
||||
{"movefocus", "r", "move focus right"},
|
||||
{"movefocus", "u", "move focus up"},
|
||||
{"movefocus", "d", "move focus down"},
|
||||
{"swapwindow", "l", "swap in left direction"},
|
||||
{"swapwindow", "r", "swap in right direction"},
|
||||
{"swapwindow", "u", "swap in up direction"},
|
||||
{"swapwindow", "d", "swap in down direction"},
|
||||
{"movetoworkspace", "+1", "move to right workspace (non-silent)"},
|
||||
{"movetoworkspace", "-1", "move to left workspace (non-silent)"},
|
||||
{"movetoworkspace", "3", "move to workspace 3 (non-silent)"},
|
||||
{"movetoworkspacesilent", "+1", "move to right workspace"},
|
||||
{"movetoworkspacesilent", "-1", "move to right workspace"},
|
||||
{"movetoworkspacesilent", "2", "move to workspace 2"},
|
||||
{"togglespecialworkspace", "", "toggle special"},
|
||||
{"exec", "firefox", "firefox"},
|
||||
{"unknown", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.dispatcher+"_"+tt.params, func(t *testing.T) {
|
||||
result := autogenerateComment(tt.dispatcher, tt.params)
|
||||
if result != tt.expected {
|
||||
t.Errorf("autogenerateComment(%q, %q) = %q, want %q",
|
||||
tt.dispatcher, tt.params, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *KeyBinding
|
||||
}{
|
||||
{
|
||||
name: "basic_keybind",
|
||||
line: "bind = SUPER, Q, killactive",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "Q",
|
||||
Dispatcher: "killactive",
|
||||
Params: "",
|
||||
Comment: "Close window",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_params",
|
||||
line: "bind = SUPER, left, movefocus, l",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "left",
|
||||
Dispatcher: "movefocus",
|
||||
Params: "l",
|
||||
Comment: "move focus left",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_comment",
|
||||
line: "bind = SUPER, T, exec, kitty # Open terminal",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "T",
|
||||
Dispatcher: "exec",
|
||||
Params: "kitty",
|
||||
Comment: "Open terminal",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_hidden",
|
||||
line: "bind = SUPER, H, exec, secret # [hidden]",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_mods",
|
||||
line: "bind = SUPER+SHIFT, F, fullscreen, 0",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "F",
|
||||
Dispatcher: "fullscreen",
|
||||
Params: "0",
|
||||
Comment: "Toggle fullscreen",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_no_mods",
|
||||
line: "bind = , Print, exec, screenshot",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
Dispatcher: "exec",
|
||||
Params: "screenshot",
|
||||
Comment: "screenshot",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Errorf("expected nil, got %+v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Errorf("expected %+v, got nil", tt.expected)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Key != tt.expected.Key {
|
||||
t.Errorf("Key = %q, want %q", result.Key, tt.expected.Key)
|
||||
}
|
||||
if result.Dispatcher != tt.expected.Dispatcher {
|
||||
t.Errorf("Dispatcher = %q, want %q", result.Dispatcher, tt.expected.Dispatcher)
|
||||
}
|
||||
if result.Params != tt.expected.Params {
|
||||
t.Errorf("Params = %q, want %q", result.Params, tt.expected.Params)
|
||||
}
|
||||
if result.Comment != tt.expected.Comment {
|
||||
t.Errorf("Comment = %q, want %q", result.Comment, tt.expected.Comment)
|
||||
}
|
||||
if len(result.Mods) != len(tt.expected.Mods) {
|
||||
t.Errorf("Mods length = %d, want %d", len(result.Mods), len(tt.expected.Mods))
|
||||
} else {
|
||||
for i := range result.Mods {
|
||||
if result.Mods[i] != tt.expected.Mods[i] {
|
||||
t.Errorf("Mods[%d] = %q, want %q", i, result.Mods[i], tt.expected.Mods[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeysWithSections(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "hyprland.conf")
|
||||
|
||||
content := `##! Window Management
|
||||
bind = SUPER, Q, killactive
|
||||
bind = SUPER, F, fullscreen, 0
|
||||
|
||||
###! Movement
|
||||
bind = SUPER, left, movefocus, l
|
||||
bind = SUPER, right, movefocus, r
|
||||
|
||||
##! Applications
|
||||
bind = SUPER, T, exec, kitty # Terminal
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Children) != 2 {
|
||||
t.Errorf("Expected 2 top-level sections, got %d", len(section.Children))
|
||||
}
|
||||
|
||||
if len(section.Children) >= 1 {
|
||||
windowMgmt := section.Children[0]
|
||||
if windowMgmt.Name != "Window Management" {
|
||||
t.Errorf("First section name = %q, want %q", windowMgmt.Name, "Window Management")
|
||||
}
|
||||
if len(windowMgmt.Keybinds) != 2 {
|
||||
t.Errorf("Window Management keybinds = %d, want 2", len(windowMgmt.Keybinds))
|
||||
}
|
||||
|
||||
if len(windowMgmt.Children) != 1 {
|
||||
t.Errorf("Window Management children = %d, want 1", len(windowMgmt.Children))
|
||||
} else {
|
||||
movement := windowMgmt.Children[0]
|
||||
if movement.Name != "Movement" {
|
||||
t.Errorf("Movement section name = %q, want %q", movement.Name, "Movement")
|
||||
}
|
||||
if len(movement.Keybinds) != 2 {
|
||||
t.Errorf("Movement keybinds = %d, want 2", len(movement.Keybinds))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(section.Children) >= 2 {
|
||||
apps := section.Children[1]
|
||||
if apps.Name != "Applications" {
|
||||
t.Errorf("Second section name = %q, want %q", apps.Name, "Applications")
|
||||
}
|
||||
if len(apps.Keybinds) != 1 {
|
||||
t.Errorf("Applications keybinds = %d, want 1", len(apps.Keybinds))
|
||||
}
|
||||
if len(apps.Keybinds) > 0 && apps.Keybinds[0].Comment != "Terminal" {
|
||||
t.Errorf("Applications keybind comment = %q, want %q", apps.Keybinds[0].Comment, "Terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeysWithCommentBinds(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "test.conf")
|
||||
|
||||
content := `#/# = SUPER, A, exec, app1
|
||||
bind = SUPER, B, exec, app2
|
||||
#/# = SUPER, C, exec, app3 # Custom comment
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) != 3 {
|
||||
t.Errorf("Expected 3 keybinds, got %d", len(section.Keybinds))
|
||||
}
|
||||
|
||||
if len(section.Keybinds) > 0 && section.Keybinds[0].Key != "A" {
|
||||
t.Errorf("First keybind key = %q, want %q", section.Keybinds[0].Key, "A")
|
||||
}
|
||||
if len(section.Keybinds) > 1 && section.Keybinds[1].Key != "B" {
|
||||
t.Errorf("Second keybind key = %q, want %q", section.Keybinds[1].Key, "B")
|
||||
}
|
||||
if len(section.Keybinds) > 2 && section.Keybinds[2].Comment != "Custom comment" {
|
||||
t.Errorf("Third keybind comment = %q, want %q", section.Keybinds[2].Comment, "Custom comment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentMultipleFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file1 := filepath.Join(tmpDir, "a.conf")
|
||||
file2 := filepath.Join(tmpDir, "b.conf")
|
||||
|
||||
content1 := "bind = SUPER, Q, killactive\n"
|
||||
content2 := "bind = SUPER, T, exec, kitty\n"
|
||||
|
||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file1: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file2: %v", err)
|
||||
}
|
||||
|
||||
parser := NewParser()
|
||||
if err := parser.ReadContent(tmpDir); err != nil {
|
||||
t.Fatalf("ReadContent failed: %v", err)
|
||||
}
|
||||
|
||||
section := parser.ParseKeys()
|
||||
if len(section.Keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds from multiple files, got %d", len(section.Keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "nonexistent_directory",
|
||||
path: "/nonexistent/path/that/does/not/exist",
|
||||
},
|
||||
{
|
||||
name: "empty_directory",
|
||||
path: t.TempDir(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseKeys(tt.path)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentWithTildeExpansion(t *testing.T) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||
t.Skip("Cannot create test directory in home")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
configFile := filepath.Join(tmpSubdir, "test.conf")
|
||||
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(homeDir, tmpSubdir)
|
||||
if err != nil {
|
||||
t.Skip("Cannot create relative path")
|
||||
}
|
||||
|
||||
parser := NewParser()
|
||||
tildePathMatch := "~/" + relPath
|
||||
err = parser.ReadContent(tildePathMatch)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ReadContent with tilde path failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeybindWithParamsContainingCommas(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
|
||||
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected keybind, got nil")
|
||||
}
|
||||
|
||||
expected := "notify-send 'Title' 'Message, with comma'"
|
||||
if result.Params != expected {
|
||||
t.Errorf("Params = %q, want %q", result.Params, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyAndCommentLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "test.conf")
|
||||
|
||||
content := `
|
||||
# This is a comment
|
||||
bind = SUPER, Q, killactive
|
||||
|
||||
# Another comment
|
||||
|
||||
bind = SUPER, T, exec, kitty
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
||||
}
|
||||
}
|
||||
125
internal/keybinds/discovery.go
Normal file
125
internal/keybinds/discovery.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package keybinds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DiscoveryConfig struct {
|
||||
SearchPaths []string
|
||||
}
|
||||
|
||||
func DefaultDiscoveryConfig() *DiscoveryConfig {
|
||||
var searchPaths []string
|
||||
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
configHome = filepath.Join(homeDir, ".config")
|
||||
}
|
||||
}
|
||||
|
||||
if configHome != "" {
|
||||
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
|
||||
}
|
||||
|
||||
configDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||
if configDirs != "" {
|
||||
for _, dir := range strings.Split(configDirs, ":") {
|
||||
if dir != "" {
|
||||
searchPaths = append(searchPaths, filepath.Join(dir, "DankMaterialShell", "cheatsheets"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &DiscoveryConfig{
|
||||
SearchPaths: searchPaths,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) {
|
||||
var files []string
|
||||
|
||||
for _, searchPath := range d.SearchPaths {
|
||||
expandedPath, err := expandPath(searchPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := os.Stat(expandedPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(expandedPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(expandedPath, entry.Name())
|
||||
files = append(files, fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func expandPath(path string) (string, error) {
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
|
||||
if filepath.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
return filepath.Clean(expandedPath), nil
|
||||
}
|
||||
|
||||
type JSONProviderFactory func(filePath string) (Provider, error)
|
||||
|
||||
var jsonProviderFactory JSONProviderFactory
|
||||
|
||||
func SetJSONProviderFactory(factory JSONProviderFactory) {
|
||||
jsonProviderFactory = factory
|
||||
}
|
||||
|
||||
func AutoDiscoverProviders(registry *Registry, config *DiscoveryConfig) error {
|
||||
if config == nil {
|
||||
config = DefaultDiscoveryConfig()
|
||||
}
|
||||
|
||||
if jsonProviderFactory == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
files, err := config.FindJSONFiles()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to discover JSON files: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
provider, err := jsonProviderFactory(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := registry.Register(provider); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
285
internal/keybinds/discovery_test.go
Normal file
285
internal/keybinds/discovery_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package keybinds
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultDiscoveryConfig(t *testing.T) {
|
||||
oldConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
oldConfigDirs := os.Getenv("XDG_CONFIG_DIRS")
|
||||
defer func() {
|
||||
os.Setenv("XDG_CONFIG_HOME", oldConfigHome)
|
||||
os.Setenv("XDG_CONFIG_DIRS", oldConfigDirs)
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
configHome string
|
||||
configDirs string
|
||||
expectedCount int
|
||||
checkFirstPath bool
|
||||
firstPath string
|
||||
}{
|
||||
{
|
||||
name: "default with no XDG vars",
|
||||
configHome: "",
|
||||
configDirs: "",
|
||||
expectedCount: 1,
|
||||
checkFirstPath: true,
|
||||
},
|
||||
{
|
||||
name: "with XDG_CONFIG_HOME set",
|
||||
configHome: "/custom/config",
|
||||
configDirs: "",
|
||||
expectedCount: 1,
|
||||
checkFirstPath: true,
|
||||
firstPath: "/custom/config/DankMaterialShell/cheatsheets",
|
||||
},
|
||||
{
|
||||
name: "with XDG_CONFIG_DIRS set",
|
||||
configHome: "/home/user/.config",
|
||||
configDirs: "/etc/xdg:/opt/config",
|
||||
expectedCount: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
os.Setenv("XDG_CONFIG_HOME", tt.configHome)
|
||||
os.Setenv("XDG_CONFIG_DIRS", tt.configDirs)
|
||||
|
||||
config := DefaultDiscoveryConfig()
|
||||
|
||||
if config == nil {
|
||||
t.Fatal("DefaultDiscoveryConfig returned nil")
|
||||
}
|
||||
|
||||
if len(config.SearchPaths) != tt.expectedCount {
|
||||
t.Errorf("SearchPaths count = %d, want %d", len(config.SearchPaths), tt.expectedCount)
|
||||
}
|
||||
|
||||
if tt.checkFirstPath && len(config.SearchPaths) > 0 {
|
||||
if tt.firstPath != "" && config.SearchPaths[0] != tt.firstPath {
|
||||
t.Errorf("SearchPaths[0] = %q, want %q", config.SearchPaths[0], tt.firstPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindJSONFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file1 := filepath.Join(tmpDir, "tmux.json")
|
||||
file2 := filepath.Join(tmpDir, "vim.json")
|
||||
txtFile := filepath.Join(tmpDir, "readme.txt")
|
||||
subdir := filepath.Join(tmpDir, "subdir")
|
||||
|
||||
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create file1: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create file2: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(txtFile, []byte("text"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create txt file: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(subdir, 0755); err != nil {
|
||||
t.Fatalf("Failed to create subdir: %v", err)
|
||||
}
|
||||
|
||||
config := &DiscoveryConfig{
|
||||
SearchPaths: []string{tmpDir},
|
||||
}
|
||||
|
||||
files, err := config.FindJSONFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("FindJSONFiles failed: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != 2 {
|
||||
t.Errorf("expected 2 JSON files, got %d", len(files))
|
||||
}
|
||||
|
||||
found := make(map[string]bool)
|
||||
for _, f := range files {
|
||||
found[filepath.Base(f)] = true
|
||||
}
|
||||
|
||||
if !found["tmux.json"] {
|
||||
t.Error("tmux.json not found")
|
||||
}
|
||||
if !found["vim.json"] {
|
||||
t.Error("vim.json not found")
|
||||
}
|
||||
if found["readme.txt"] {
|
||||
t.Error("readme.txt should not be included")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindJSONFilesNonexistentPath(t *testing.T) {
|
||||
config := &DiscoveryConfig{
|
||||
SearchPaths: []string{"/nonexistent/path"},
|
||||
}
|
||||
|
||||
files, err := config.FindJSONFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("FindJSONFiles failed: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != 0 {
|
||||
t.Errorf("expected 0 files for nonexistent path, got %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindJSONFilesMultiplePaths(t *testing.T) {
|
||||
tmpDir1 := t.TempDir()
|
||||
tmpDir2 := t.TempDir()
|
||||
|
||||
file1 := filepath.Join(tmpDir1, "app1.json")
|
||||
file2 := filepath.Join(tmpDir2, "app2.json")
|
||||
|
||||
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create file1: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create file2: %v", err)
|
||||
}
|
||||
|
||||
config := &DiscoveryConfig{
|
||||
SearchPaths: []string{tmpDir1, tmpDir2},
|
||||
}
|
||||
|
||||
files, err := config.FindJSONFiles()
|
||||
if err != nil {
|
||||
t.Fatalf("FindJSONFiles failed: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != 2 {
|
||||
t.Errorf("expected 2 JSON files from multiple paths, got %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoDiscoverProviders(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
jsonContent := `{
|
||||
"title": "Test App",
|
||||
"provider": "testapp",
|
||||
"binds": {}
|
||||
}`
|
||||
|
||||
file := filepath.Join(tmpDir, "testapp.json")
|
||||
if err := os.WriteFile(file, []byte(jsonContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
config := &DiscoveryConfig{
|
||||
SearchPaths: []string{tmpDir},
|
||||
}
|
||||
|
||||
registry := NewRegistry()
|
||||
|
||||
factoryCalled := false
|
||||
SetJSONProviderFactory(func(filePath string) (Provider, error) {
|
||||
factoryCalled = true
|
||||
return &mockProvider{name: "testapp"}, nil
|
||||
})
|
||||
|
||||
err := AutoDiscoverProviders(registry, config)
|
||||
if err != nil {
|
||||
t.Fatalf("AutoDiscoverProviders failed: %v", err)
|
||||
}
|
||||
|
||||
if !factoryCalled {
|
||||
t.Error("factory was not called")
|
||||
}
|
||||
|
||||
provider, err := registry.Get("testapp")
|
||||
if err != nil {
|
||||
t.Fatalf("provider not registered: %v", err)
|
||||
}
|
||||
|
||||
if provider.Name() != "testapp" {
|
||||
t.Errorf("provider name = %q, want %q", provider.Name(), "testapp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoDiscoverProvidersNilConfig(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
|
||||
SetJSONProviderFactory(func(filePath string) (Provider, error) {
|
||||
return &mockProvider{name: "test"}, nil
|
||||
})
|
||||
|
||||
err := AutoDiscoverProviders(registry, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("AutoDiscoverProviders with nil config failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoDiscoverProvidersNoFactory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file := filepath.Join(tmpDir, "test.json")
|
||||
if err := os.WriteFile(file, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
config := &DiscoveryConfig{
|
||||
SearchPaths: []string{tmpDir},
|
||||
}
|
||||
|
||||
registry := NewRegistry()
|
||||
|
||||
SetJSONProviderFactory(nil)
|
||||
|
||||
err := AutoDiscoverProviders(registry, config)
|
||||
if err != nil {
|
||||
t.Fatalf("AutoDiscoverProviders should not fail without factory: %v", err)
|
||||
}
|
||||
|
||||
providers := registry.List()
|
||||
if len(providers) != 0 {
|
||||
t.Errorf("expected 0 providers without factory, got %d", len(providers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPathInDiscovery(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "tilde expansion",
|
||||
input: "~/test",
|
||||
expected: filepath.Join(home, "test"),
|
||||
},
|
||||
{
|
||||
name: "absolute path",
|
||||
input: "/tmp/test",
|
||||
expected: "/tmp/test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := expandPath(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("expandPath failed: %v", err)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
116
internal/keybinds/providers/hyprland.go
Normal file
116
internal/keybinds/providers/hyprland.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/hyprland"
|
||||
"github.com/AvengeMedia/danklinux/internal/keybinds"
|
||||
)
|
||||
|
||||
type HyprlandProvider struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func NewHyprlandProvider(configPath string) *HyprlandProvider {
|
||||
if configPath == "" {
|
||||
configPath = "$HOME/.config/hypr"
|
||||
}
|
||||
return &HyprlandProvider{
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) Name() string {
|
||||
return "hyprland"
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
section, err := hyprland.ParseKeys(h.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
|
||||
}
|
||||
|
||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||
h.convertSection(section, "", categorizedBinds)
|
||||
|
||||
return &keybinds.CheatSheet{
|
||||
Title: "Hyprland Keybinds",
|
||||
Provider: h.Name(),
|
||||
Binds: categorizedBinds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) convertSection(section *hyprland.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||
currentSubcat := subcategory
|
||||
if section.Name != "" {
|
||||
currentSubcat = section.Name
|
||||
}
|
||||
|
||||
for _, kb := range section.Keybinds {
|
||||
category := h.categorizeByDispatcher(kb.Dispatcher)
|
||||
bind := h.convertKeybind(&kb, currentSubcat)
|
||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||
}
|
||||
|
||||
for _, child := range section.Children {
|
||||
h.convertSection(&child, currentSubcat, categorizedBinds)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
||||
switch {
|
||||
case strings.Contains(dispatcher, "workspace"):
|
||||
return "Workspace"
|
||||
case strings.Contains(dispatcher, "monitor"):
|
||||
return "Monitor"
|
||||
case strings.Contains(dispatcher, "window") ||
|
||||
strings.Contains(dispatcher, "focus") ||
|
||||
strings.Contains(dispatcher, "move") ||
|
||||
strings.Contains(dispatcher, "swap") ||
|
||||
strings.Contains(dispatcher, "resize") ||
|
||||
dispatcher == "killactive" ||
|
||||
dispatcher == "fullscreen" ||
|
||||
dispatcher == "togglefloating" ||
|
||||
dispatcher == "pin" ||
|
||||
dispatcher == "fakefullscreen" ||
|
||||
dispatcher == "splitratio" ||
|
||||
dispatcher == "resizeactive":
|
||||
return "Window"
|
||||
case dispatcher == "exec":
|
||||
return "Execute"
|
||||
case dispatcher == "exit" || strings.Contains(dispatcher, "dpms"):
|
||||
return "System"
|
||||
default:
|
||||
return "Other"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) convertKeybind(kb *hyprland.KeyBinding, subcategory string) keybinds.Keybind {
|
||||
key := h.formatKey(kb)
|
||||
desc := kb.Comment
|
||||
|
||||
if desc == "" {
|
||||
desc = h.generateDescription(kb.Dispatcher, kb.Params)
|
||||
}
|
||||
|
||||
return keybinds.Keybind{
|
||||
Key: key,
|
||||
Description: desc,
|
||||
Subcategory: subcategory,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) generateDescription(dispatcher, params string) string {
|
||||
if params != "" {
|
||||
return dispatcher + " " + params
|
||||
}
|
||||
return dispatcher
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) formatKey(kb *hyprland.KeyBinding) string {
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
225
internal/keybinds/providers/hyprland_test.go
Normal file
225
internal/keybinds/providers/hyprland_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewHyprlandProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configPath string
|
||||
wantPath string
|
||||
}{
|
||||
{
|
||||
name: "custom path",
|
||||
configPath: "/custom/path",
|
||||
wantPath: "/custom/path",
|
||||
},
|
||||
{
|
||||
name: "empty path defaults",
|
||||
configPath: "",
|
||||
wantPath: "$HOME/.config/hypr",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewHyprlandProvider(tt.configPath)
|
||||
if p == nil {
|
||||
t.Fatal("NewHyprlandProvider returned nil")
|
||||
}
|
||||
|
||||
if p.configPath != tt.wantPath {
|
||||
t.Errorf("configPath = %q, want %q", p.configPath, tt.wantPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandProviderName(t *testing.T) {
|
||||
p := NewHyprlandProvider("")
|
||||
if p.Name() != "hyprland" {
|
||||
t.Errorf("Name() = %q, want %q", p.Name(), "hyprland")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandProviderGetCheatSheet(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "hyprland.conf")
|
||||
|
||||
content := `##! Window Management
|
||||
bind = SUPER, Q, killactive
|
||||
bind = SUPER, F, fullscreen, 0
|
||||
|
||||
###! Movement
|
||||
bind = SUPER, left, movefocus, l
|
||||
bind = SUPER, right, movefocus, r
|
||||
|
||||
##! Applications
|
||||
bind = SUPER, T, exec, kitty # Terminal
|
||||
bind = SUPER, 1, workspace, 1
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
p := NewHyprlandProvider(tmpDir)
|
||||
sheet, err := p.GetCheatSheet()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
if sheet.Title != "Hyprland Keybinds" {
|
||||
t.Errorf("Title = %q, want %q", sheet.Title, "Hyprland Keybinds")
|
||||
}
|
||||
|
||||
if sheet.Provider != "hyprland" {
|
||||
t.Errorf("Provider = %q, want %q", sheet.Provider, "hyprland")
|
||||
}
|
||||
|
||||
if len(sheet.Binds) == 0 {
|
||||
t.Error("expected categorized bindings, got none")
|
||||
}
|
||||
|
||||
if windowBinds, ok := sheet.Binds["Window"]; !ok || len(windowBinds) == 0 {
|
||||
t.Error("expected Window category with bindings")
|
||||
}
|
||||
|
||||
if execBinds, ok := sheet.Binds["Execute"]; !ok || len(execBinds) == 0 {
|
||||
t.Error("expected Execute category with bindings")
|
||||
}
|
||||
|
||||
if wsBinds, ok := sheet.Binds["Workspace"]; !ok || len(wsBinds) == 0 {
|
||||
t.Error("expected Workspace category with bindings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandProviderGetCheatSheetError(t *testing.T) {
|
||||
p := NewHyprlandProvider("/nonexistent/path")
|
||||
_, err := p.GetCheatSheet()
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent path, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "test.conf")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
expected string
|
||||
category string
|
||||
}{
|
||||
{
|
||||
name: "single mod",
|
||||
content: "bind = SUPER, Q, killactive",
|
||||
expected: "SUPER+Q",
|
||||
category: "Window",
|
||||
},
|
||||
{
|
||||
name: "multiple mods",
|
||||
content: "bind = SUPER+SHIFT, F, fullscreen, 0",
|
||||
expected: "SUPER+SHIFT+F",
|
||||
category: "Window",
|
||||
},
|
||||
{
|
||||
name: "no mods",
|
||||
content: "bind = , Print, exec, screenshot",
|
||||
expected: "Print",
|
||||
category: "Execute",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
p := NewHyprlandProvider(tmpDir)
|
||||
sheet, err := p.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
categoryBinds, ok := sheet.Binds[tt.category]
|
||||
if !ok || len(categoryBinds) == 0 {
|
||||
t.Fatalf("expected binds in category %q", tt.category)
|
||||
}
|
||||
|
||||
if categoryBinds[0].Key != tt.expected {
|
||||
t.Errorf("Key = %q, want %q", categoryBinds[0].Key, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionFallback(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "test.conf")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
wantDesc string
|
||||
}{
|
||||
{
|
||||
name: "autogenerated description for known dispatcher",
|
||||
content: "bind = SUPER, Q, killactive",
|
||||
wantDesc: "Close window",
|
||||
},
|
||||
{
|
||||
name: "custom comment overrides autogeneration",
|
||||
content: "bind = SUPER, T, exec, kitty # Open terminal",
|
||||
wantDesc: "Open terminal",
|
||||
},
|
||||
{
|
||||
name: "fallback for unknown dispatcher without params",
|
||||
content: "bind = SUPER, W, unknowndispatcher",
|
||||
wantDesc: "unknowndispatcher",
|
||||
},
|
||||
{
|
||||
name: "fallback for unknown dispatcher with params",
|
||||
content: "bind = SUPER, X, customdispatcher, arg1",
|
||||
wantDesc: "customdispatcher arg1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
p := NewHyprlandProvider(tmpDir)
|
||||
sheet, err := p.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, binds := range sheet.Binds {
|
||||
for _, bind := range binds {
|
||||
if bind.Description == tt.wantDesc {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("expected description %q not found in any bind", tt.wantDesc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
130
internal/keybinds/providers/jsonfile.go
Normal file
130
internal/keybinds/providers/jsonfile.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/keybinds"
|
||||
)
|
||||
|
||||
type JSONFileProvider struct {
|
||||
filePath string
|
||||
name string
|
||||
}
|
||||
|
||||
func NewJSONFileProvider(filePath string) (*JSONFileProvider, error) {
|
||||
if filePath == "" {
|
||||
return nil, fmt.Errorf("file path cannot be empty")
|
||||
}
|
||||
|
||||
expandedPath, err := expandPath(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to expand path: %w", err)
|
||||
}
|
||||
|
||||
name := filepath.Base(expandedPath)
|
||||
name = name[:len(name)-len(filepath.Ext(name))]
|
||||
|
||||
return &JSONFileProvider{
|
||||
filePath: expandedPath,
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (j *JSONFileProvider) Name() string {
|
||||
return j.name
|
||||
}
|
||||
|
||||
func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
data, err := os.ReadFile(j.filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
var rawData map[string]interface{}
|
||||
if err := json.Unmarshal(data, &rawData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
title, _ := rawData["title"].(string)
|
||||
provider, _ := rawData["provider"].(string)
|
||||
if provider == "" {
|
||||
provider = j.name
|
||||
}
|
||||
|
||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||
|
||||
bindsRaw, ok := rawData["binds"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing 'binds' field")
|
||||
}
|
||||
|
||||
switch binds := bindsRaw.(type) {
|
||||
case map[string]interface{}:
|
||||
for category, categoryBindsRaw := range binds {
|
||||
categoryBindsList, ok := categoryBindsRaw.([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var keybindsList []keybinds.Keybind
|
||||
categoryBindsJSON, _ := json.Marshal(categoryBindsList)
|
||||
if err := json.Unmarshal(categoryBindsJSON, &keybindsList); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
categorizedBinds[category] = keybindsList
|
||||
}
|
||||
|
||||
case []interface{}:
|
||||
flatBindsJSON, _ := json.Marshal(binds)
|
||||
var flatBinds []struct {
|
||||
Key string `json:"key"`
|
||||
Description string `json:"desc"`
|
||||
Category string `json:"cat,omitempty"`
|
||||
Subcategory string `json:"subcat,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(flatBindsJSON, &flatBinds); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse flat binds array: %w", err)
|
||||
}
|
||||
|
||||
for _, bind := range flatBinds {
|
||||
category := bind.Category
|
||||
if category == "" {
|
||||
category = "Other"
|
||||
}
|
||||
|
||||
kb := keybinds.Keybind{
|
||||
Key: bind.Key,
|
||||
Description: bind.Description,
|
||||
Subcategory: bind.Subcategory,
|
||||
}
|
||||
categorizedBinds[category] = append(categorizedBinds[category], kb)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("'binds' must be either an object (categorized) or array (flat)")
|
||||
}
|
||||
|
||||
return &keybinds.CheatSheet{
|
||||
Title: title,
|
||||
Provider: provider,
|
||||
Binds: categorizedBinds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func expandPath(path string) (string, error) {
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
|
||||
if filepath.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
return filepath.Clean(expandedPath), nil
|
||||
}
|
||||
279
internal/keybinds/providers/jsonfile_test.go
Normal file
279
internal/keybinds/providers/jsonfile_test.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewJSONFileProvider(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.json")
|
||||
|
||||
if err := os.WriteFile(testFile, []byte("{}"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create test file: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filePath string
|
||||
expectError bool
|
||||
wantName string
|
||||
}{
|
||||
{
|
||||
name: "valid file",
|
||||
filePath: testFile,
|
||||
expectError: false,
|
||||
wantName: "test",
|
||||
},
|
||||
{
|
||||
name: "empty path",
|
||||
filePath: "",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := NewJSONFileProvider(tt.filePath)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if p.Name() != tt.wantName {
|
||||
t.Errorf("Name() = %q, want %q", p.Name(), tt.wantName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFileProviderGetCheatSheet(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "tmux.json")
|
||||
|
||||
content := `{
|
||||
"title": "Tmux Binds",
|
||||
"provider": "tmux",
|
||||
"binds": {
|
||||
"Pane": [
|
||||
{
|
||||
"key": "Ctrl+Alt+J",
|
||||
"desc": "Resize split downward",
|
||||
"subcat": "Sizing"
|
||||
},
|
||||
{
|
||||
"key": "Ctrl+K",
|
||||
"desc": "Move Focus Up",
|
||||
"subcat": "Navigation"
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
p, err := NewJSONFileProvider(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONFileProvider failed: %v", err)
|
||||
}
|
||||
|
||||
sheet, err := p.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
if sheet.Title != "Tmux Binds" {
|
||||
t.Errorf("Title = %q, want %q", sheet.Title, "Tmux Binds")
|
||||
}
|
||||
|
||||
if sheet.Provider != "tmux" {
|
||||
t.Errorf("Provider = %q, want %q", sheet.Provider, "tmux")
|
||||
}
|
||||
|
||||
paneBinds, ok := sheet.Binds["Pane"]
|
||||
if !ok {
|
||||
t.Fatal("expected Pane category")
|
||||
}
|
||||
|
||||
if len(paneBinds) != 2 {
|
||||
t.Errorf("len(Pane binds) = %d, want 2", len(paneBinds))
|
||||
}
|
||||
|
||||
if len(paneBinds) > 0 {
|
||||
bind := paneBinds[0]
|
||||
if bind.Key != "Ctrl+Alt+J" {
|
||||
t.Errorf("Pane[0].Key = %q, want %q", bind.Key, "Ctrl+Alt+J")
|
||||
}
|
||||
if bind.Description != "Resize split downward" {
|
||||
t.Errorf("Pane[0].Description = %q, want %q", bind.Description, "Resize split downward")
|
||||
}
|
||||
if bind.Subcategory != "Sizing" {
|
||||
t.Errorf("Pane[0].Subcategory = %q, want %q", bind.Subcategory, "Sizing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFileProviderGetCheatSheetNoProvider(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "custom.json")
|
||||
|
||||
content := `{
|
||||
"title": "Custom Binds",
|
||||
"binds": {}
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
p, err := NewJSONFileProvider(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONFileProvider failed: %v", err)
|
||||
}
|
||||
|
||||
sheet, err := p.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
if sheet.Provider != "custom" {
|
||||
t.Errorf("Provider = %q, want %q (should default to filename)", sheet.Provider, "custom")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFileProviderFlatArrayBackwardsCompat(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "legacy.json")
|
||||
|
||||
content := `{
|
||||
"title": "Legacy Format",
|
||||
"provider": "legacy",
|
||||
"binds": [
|
||||
{
|
||||
"key": "Ctrl+S",
|
||||
"desc": "Save file",
|
||||
"cat": "File",
|
||||
"subcat": "Operations"
|
||||
},
|
||||
{
|
||||
"key": "Ctrl+O",
|
||||
"desc": "Open file",
|
||||
"cat": "File"
|
||||
},
|
||||
{
|
||||
"key": "Ctrl+Q",
|
||||
"desc": "Quit",
|
||||
"subcat": "Exit"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
p, err := NewJSONFileProvider(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONFileProvider failed: %v", err)
|
||||
}
|
||||
|
||||
sheet, err := p.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
fileBinds, ok := sheet.Binds["File"]
|
||||
if !ok || len(fileBinds) != 2 {
|
||||
t.Errorf("expected 2 binds in File category, got %d", len(fileBinds))
|
||||
}
|
||||
|
||||
otherBinds, ok := sheet.Binds["Other"]
|
||||
if !ok || len(otherBinds) != 1 {
|
||||
t.Errorf("expected 1 bind in Other category (no cat specified), got %d", len(otherBinds))
|
||||
}
|
||||
|
||||
if len(fileBinds) > 0 {
|
||||
if fileBinds[0].Subcategory != "Operations" {
|
||||
t.Errorf("expected subcategory %q, got %q", "Operations", fileBinds[0].Subcategory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFileProviderInvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "invalid.json")
|
||||
|
||||
if err := os.WriteFile(testFile, []byte("not valid json"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
|
||||
p, err := NewJSONFileProvider(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONFileProvider failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = p.GetCheatSheet()
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid JSON, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONFileProviderNonexistentFile(t *testing.T) {
|
||||
p, err := NewJSONFileProvider("/nonexistent/file.json")
|
||||
if err != nil {
|
||||
t.Fatalf("NewJSONFileProvider failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = p.GetCheatSheet()
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent file, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandPath(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "tilde expansion",
|
||||
input: "~/test",
|
||||
expected: filepath.Join(home, "test"),
|
||||
},
|
||||
{
|
||||
name: "no expansion needed",
|
||||
input: "/absolute/path",
|
||||
expected: "/absolute/path",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := expandPath(tt.input)
|
||||
if err != nil {
|
||||
t.Fatalf("expandPath failed: %v", err)
|
||||
}
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
112
internal/keybinds/providers/mangowc.go
Normal file
112
internal/keybinds/providers/mangowc.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/keybinds"
|
||||
"github.com/AvengeMedia/danklinux/internal/mangowc"
|
||||
)
|
||||
|
||||
type MangoWCProvider struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func NewMangoWCProvider(configPath string) *MangoWCProvider {
|
||||
if configPath == "" {
|
||||
configPath = "$HOME/.config/mango"
|
||||
}
|
||||
return &MangoWCProvider{
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) Name() string {
|
||||
return "mangowc"
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
keybinds_list, err := mangowc.ParseKeys(m.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
|
||||
}
|
||||
|
||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||
for _, kb := range keybinds_list {
|
||||
category := m.categorizeByCommand(kb.Command)
|
||||
bind := m.convertKeybind(&kb)
|
||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||
}
|
||||
|
||||
return &keybinds.CheatSheet{
|
||||
Title: "MangoWC Keybinds",
|
||||
Provider: m.Name(),
|
||||
Binds: categorizedBinds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
||||
switch {
|
||||
case strings.Contains(command, "mon"):
|
||||
return "Monitor"
|
||||
case command == "toggleoverview":
|
||||
return "Overview"
|
||||
case command == "toggle_scratchpad":
|
||||
return "Scratchpad"
|
||||
case strings.Contains(command, "layout") || strings.Contains(command, "proportion"):
|
||||
return "Layout"
|
||||
case strings.Contains(command, "gaps"):
|
||||
return "Gaps"
|
||||
case strings.Contains(command, "view") || strings.Contains(command, "tag"):
|
||||
return "Tags"
|
||||
case command == "focusstack" ||
|
||||
command == "focusdir" ||
|
||||
command == "exchange_client" ||
|
||||
command == "killclient" ||
|
||||
command == "togglefloating" ||
|
||||
command == "togglefullscreen" ||
|
||||
command == "togglefakefullscreen" ||
|
||||
command == "togglemaximizescreen" ||
|
||||
command == "toggleglobal" ||
|
||||
command == "toggleoverlay" ||
|
||||
command == "minimized" ||
|
||||
command == "restore_minimized" ||
|
||||
command == "movewin" ||
|
||||
command == "resizewin":
|
||||
return "Window"
|
||||
case command == "spawn" || command == "spawn_shell":
|
||||
return "Execute"
|
||||
case command == "quit" || command == "reload_config":
|
||||
return "System"
|
||||
default:
|
||||
return "Other"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) convertKeybind(kb *mangowc.KeyBinding) keybinds.Keybind {
|
||||
key := m.formatKey(kb)
|
||||
desc := kb.Comment
|
||||
|
||||
if desc == "" {
|
||||
desc = m.generateDescription(kb.Command, kb.Params)
|
||||
}
|
||||
|
||||
return keybinds.Keybind{
|
||||
Key: key,
|
||||
Description: desc,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) generateDescription(command, params string) string {
|
||||
if params != "" {
|
||||
return command + " " + params
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) formatKey(kb *mangowc.KeyBinding) string {
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
313
internal/keybinds/providers/mangowc_test.go
Normal file
313
internal/keybinds/providers/mangowc_test.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/mangowc"
|
||||
)
|
||||
|
||||
func TestMangoWCProviderName(t *testing.T) {
|
||||
provider := NewMangoWCProvider("")
|
||||
if provider.Name() != "mangowc" {
|
||||
t.Errorf("Name() = %q, want %q", provider.Name(), "mangowc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCProviderDefaultPath(t *testing.T) {
|
||||
provider := NewMangoWCProvider("")
|
||||
if provider.configPath != "$HOME/.config/mango" {
|
||||
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/mango")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCProviderCustomPath(t *testing.T) {
|
||||
customPath := "/custom/path"
|
||||
provider := NewMangoWCProvider(customPath)
|
||||
if provider.configPath != customPath {
|
||||
t.Errorf("configPath = %q, want %q", provider.configPath, customPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCCategorizeByCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
command string
|
||||
expected string
|
||||
}{
|
||||
{"view", "Tags"},
|
||||
{"tag", "Tags"},
|
||||
{"toggleview", "Tags"},
|
||||
{"viewtoleft", "Tags"},
|
||||
{"viewtoright", "Tags"},
|
||||
{"viewtoleft_have_client", "Tags"},
|
||||
{"tagtoleft", "Tags"},
|
||||
{"tagtoright", "Tags"},
|
||||
{"focusmon", "Monitor"},
|
||||
{"tagmon", "Monitor"},
|
||||
{"focusstack", "Window"},
|
||||
{"focusdir", "Window"},
|
||||
{"exchange_client", "Window"},
|
||||
{"killclient", "Window"},
|
||||
{"togglefloating", "Window"},
|
||||
{"togglefullscreen", "Window"},
|
||||
{"togglefakefullscreen", "Window"},
|
||||
{"togglemaximizescreen", "Window"},
|
||||
{"toggleglobal", "Window"},
|
||||
{"toggleoverlay", "Window"},
|
||||
{"minimized", "Window"},
|
||||
{"restore_minimized", "Window"},
|
||||
{"movewin", "Window"},
|
||||
{"resizewin", "Window"},
|
||||
{"toggleoverview", "Overview"},
|
||||
{"toggle_scratchpad", "Scratchpad"},
|
||||
{"setlayout", "Layout"},
|
||||
{"switch_layout", "Layout"},
|
||||
{"set_proportion", "Layout"},
|
||||
{"switch_proportion_preset", "Layout"},
|
||||
{"incgaps", "Gaps"},
|
||||
{"togglegaps", "Gaps"},
|
||||
{"spawn", "Execute"},
|
||||
{"spawn_shell", "Execute"},
|
||||
{"quit", "System"},
|
||||
{"reload_config", "System"},
|
||||
{"unknown_command", "Other"},
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider("")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.command, func(t *testing.T) {
|
||||
result := provider.categorizeByCommand(tt.command)
|
||||
if result != tt.expected {
|
||||
t.Errorf("categorizeByCommand(%q) = %q, want %q", tt.command, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCFormatKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *mangowc.KeyBinding
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single_mod",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "q",
|
||||
},
|
||||
expected: "ALT+q",
|
||||
},
|
||||
{
|
||||
name: "multiple_mods",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "Up",
|
||||
},
|
||||
expected: "SUPER+SHIFT+Up",
|
||||
},
|
||||
{
|
||||
name: "no_mods",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
},
|
||||
expected: "Print",
|
||||
},
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider("")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := provider.formatKey(tt.keybind)
|
||||
if result != tt.expected {
|
||||
t.Errorf("formatKey() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCConvertKeybind(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *mangowc.KeyBinding
|
||||
wantKey string
|
||||
wantDesc string
|
||||
}{
|
||||
{
|
||||
name: "with_comment",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "t",
|
||||
Command: "spawn",
|
||||
Params: "kitty",
|
||||
Comment: "Open terminal",
|
||||
},
|
||||
wantKey: "ALT+t",
|
||||
wantDesc: "Open terminal",
|
||||
},
|
||||
{
|
||||
name: "without_comment",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "r",
|
||||
Command: "reload_config",
|
||||
Params: "",
|
||||
Comment: "",
|
||||
},
|
||||
wantKey: "SUPER+r",
|
||||
wantDesc: "reload_config",
|
||||
},
|
||||
{
|
||||
name: "with_params_no_comment",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
Mods: []string{"CTRL"},
|
||||
Key: "1",
|
||||
Command: "view",
|
||||
Params: "1,0",
|
||||
Comment: "",
|
||||
},
|
||||
wantKey: "CTRL+1",
|
||||
wantDesc: "view 1,0",
|
||||
},
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider("")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := provider.convertKeybind(tt.keybind)
|
||||
if result.Key != tt.wantKey {
|
||||
t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey)
|
||||
}
|
||||
if result.Description != tt.wantDesc {
|
||||
t.Errorf("convertKeybind().Description = %q, want %q", result.Description, tt.wantDesc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCGetCheatSheet(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `# MangoWC Configuration
|
||||
blur=0
|
||||
|
||||
# Key Bindings
|
||||
bind=SUPER,r,reload_config
|
||||
bind=Alt,t,spawn,kitty # Terminal
|
||||
bind=ALT,q,killclient,
|
||||
|
||||
# Window management
|
||||
bind=ALT,Left,focusdir,left
|
||||
bind=ALT,Right,focusdir,right
|
||||
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||
|
||||
# Tags
|
||||
bind=Ctrl,1,view,1,0
|
||||
bind=Ctrl,2,view,2,0
|
||||
bind=Alt,1,tag,1,0
|
||||
|
||||
# Layout
|
||||
bind=SUPER,n,switch_layout
|
||||
|
||||
# Gaps
|
||||
bind=ALT+SHIFT,X,incgaps,1
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider(tmpDir)
|
||||
sheet, err := provider.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
if sheet == nil {
|
||||
t.Fatal("Expected non-nil CheatSheet")
|
||||
}
|
||||
|
||||
if sheet.Title != "MangoWC Keybinds" {
|
||||
t.Errorf("Title = %q, want %q", sheet.Title, "MangoWC Keybinds")
|
||||
}
|
||||
|
||||
if sheet.Provider != "mangowc" {
|
||||
t.Errorf("Provider = %q, want %q", sheet.Provider, "mangowc")
|
||||
}
|
||||
|
||||
categories := []string{"System", "Execute", "Window", "Tags", "Layout", "Gaps"}
|
||||
for _, category := range categories {
|
||||
if _, exists := sheet.Binds[category]; !exists {
|
||||
t.Errorf("Expected category %q to exist", category)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sheet.Binds["System"]) < 1 {
|
||||
t.Error("Expected at least 1 System keybind")
|
||||
}
|
||||
if len(sheet.Binds["Execute"]) < 1 {
|
||||
t.Error("Expected at least 1 Execute keybind")
|
||||
}
|
||||
if len(sheet.Binds["Window"]) < 3 {
|
||||
t.Error("Expected at least 3 Window keybinds")
|
||||
}
|
||||
if len(sheet.Binds["Tags"]) < 3 {
|
||||
t.Error("Expected at least 3 Tags keybinds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCGetCheatSheetError(t *testing.T) {
|
||||
provider := NewMangoWCProvider("/nonexistent/path")
|
||||
_, err := provider.GetCheatSheet()
|
||||
if err == nil {
|
||||
t.Error("Expected error for nonexistent path, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCIntegration(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `bind=Alt,t,spawn,kitty # Open terminal
|
||||
bind=ALT,q,killclient,
|
||||
bind=SUPER,r,reload_config # Reload config
|
||||
bind=ALT,Left,focusdir,left
|
||||
bind=Ctrl,1,view,1,0
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider(tmpDir)
|
||||
sheet, err := provider.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
totalBinds := 0
|
||||
for _, binds := range sheet.Binds {
|
||||
totalBinds += len(binds)
|
||||
}
|
||||
|
||||
expectedBinds := 5
|
||||
if totalBinds != expectedBinds {
|
||||
t.Errorf("Expected %d total keybinds, got %d", expectedBinds, totalBinds)
|
||||
}
|
||||
|
||||
foundTerminal := false
|
||||
for _, binds := range sheet.Binds {
|
||||
for _, bind := range binds {
|
||||
if bind.Description == "Open terminal" && bind.Key == "Alt+t" {
|
||||
foundTerminal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundTerminal {
|
||||
t.Error("Did not find terminal keybind with correct key and description")
|
||||
}
|
||||
}
|
||||
112
internal/keybinds/providers/sway.go
Normal file
112
internal/keybinds/providers/sway.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/keybinds"
|
||||
"github.com/AvengeMedia/danklinux/internal/sway"
|
||||
)
|
||||
|
||||
type SwayProvider struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
func NewSwayProvider(configPath string) *SwayProvider {
|
||||
if configPath == "" {
|
||||
configPath = "$HOME/.config/sway"
|
||||
}
|
||||
return &SwayProvider{
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SwayProvider) Name() string {
|
||||
return "sway"
|
||||
}
|
||||
|
||||
func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
section, err := sway.ParseKeys(s.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sway config: %w", err)
|
||||
}
|
||||
|
||||
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||
s.convertSection(section, "", categorizedBinds)
|
||||
|
||||
return &keybinds.CheatSheet{
|
||||
Title: "Sway Keybinds",
|
||||
Provider: s.Name(),
|
||||
Binds: categorizedBinds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SwayProvider) convertSection(section *sway.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||
currentSubcat := subcategory
|
||||
if section.Name != "" {
|
||||
currentSubcat = section.Name
|
||||
}
|
||||
|
||||
for _, kb := range section.Keybinds {
|
||||
category := s.categorizeByCommand(kb.Command)
|
||||
bind := s.convertKeybind(&kb, currentSubcat)
|
||||
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||
}
|
||||
|
||||
for _, child := range section.Children {
|
||||
s.convertSection(&child, currentSubcat, categorizedBinds)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SwayProvider) categorizeByCommand(command string) string {
|
||||
command = strings.ToLower(command)
|
||||
|
||||
switch {
|
||||
case strings.Contains(command, "scratchpad"):
|
||||
return "Scratchpad"
|
||||
case strings.Contains(command, "workspace") && strings.Contains(command, "output"):
|
||||
return "Monitor"
|
||||
case strings.Contains(command, "workspace"):
|
||||
return "Workspace"
|
||||
case strings.Contains(command, "output"):
|
||||
return "Monitor"
|
||||
case strings.Contains(command, "layout"):
|
||||
return "Layout"
|
||||
case command == "kill" ||
|
||||
command == "fullscreen" || strings.Contains(command, "fullscreen") ||
|
||||
command == "floating toggle" || strings.Contains(command, "floating") ||
|
||||
strings.Contains(command, "focus") ||
|
||||
strings.Contains(command, "move") ||
|
||||
strings.Contains(command, "resize") ||
|
||||
strings.Contains(command, "split"):
|
||||
return "Window"
|
||||
case strings.HasPrefix(command, "exec"):
|
||||
return "Execute"
|
||||
case command == "exit" || command == "reload":
|
||||
return "System"
|
||||
default:
|
||||
return "Other"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) keybinds.Keybind {
|
||||
key := s.formatKey(kb)
|
||||
desc := kb.Comment
|
||||
|
||||
if desc == "" {
|
||||
desc = kb.Command
|
||||
}
|
||||
|
||||
return keybinds.Keybind{
|
||||
Key: key,
|
||||
Description: desc,
|
||||
Subcategory: subcategory,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SwayProvider) formatKey(kb *sway.KeyBinding) string {
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
290
internal/keybinds/providers/sway_test.go
Normal file
290
internal/keybinds/providers/sway_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/sway"
|
||||
)
|
||||
|
||||
func TestSwayProviderName(t *testing.T) {
|
||||
provider := NewSwayProvider("")
|
||||
if provider.Name() != "sway" {
|
||||
t.Errorf("Name() = %q, want %q", provider.Name(), "sway")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayProviderDefaultPath(t *testing.T) {
|
||||
provider := NewSwayProvider("")
|
||||
if provider.configPath != "$HOME/.config/sway" {
|
||||
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayProviderCustomPath(t *testing.T) {
|
||||
customPath := "/custom/path"
|
||||
provider := NewSwayProvider(customPath)
|
||||
if provider.configPath != customPath {
|
||||
t.Errorf("configPath = %q, want %q", provider.configPath, customPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayCategorizeByCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
command string
|
||||
expected string
|
||||
}{
|
||||
{"workspace number 1", "Workspace"},
|
||||
{"workspace prev", "Workspace"},
|
||||
{"workspace next", "Workspace"},
|
||||
{"move container to workspace number 1", "Workspace"},
|
||||
{"focus output left", "Monitor"},
|
||||
{"move workspace to output right", "Monitor"},
|
||||
{"kill", "Window"},
|
||||
{"fullscreen toggle", "Window"},
|
||||
{"floating toggle", "Window"},
|
||||
{"focus left", "Window"},
|
||||
{"focus right", "Window"},
|
||||
{"move left", "Window"},
|
||||
{"move right", "Window"},
|
||||
{"resize grow width 10px", "Window"},
|
||||
{"splith", "Window"},
|
||||
{"splitv", "Window"},
|
||||
{"layout tabbed", "Layout"},
|
||||
{"layout stacking", "Layout"},
|
||||
{"move scratchpad", "Scratchpad"},
|
||||
{"scratchpad show", "Scratchpad"},
|
||||
{"exec kitty", "Execute"},
|
||||
{"exec --no-startup-id firefox", "Execute"},
|
||||
{"exit", "System"},
|
||||
{"reload", "System"},
|
||||
{"unknown command", "Other"},
|
||||
}
|
||||
|
||||
provider := NewSwayProvider("")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.command, func(t *testing.T) {
|
||||
result := provider.categorizeByCommand(tt.command)
|
||||
if result != tt.expected {
|
||||
t.Errorf("categorizeByCommand(%q) = %q, want %q", tt.command, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayFormatKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *sway.KeyBinding
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single_mod",
|
||||
keybind: &sway.KeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "q",
|
||||
},
|
||||
expected: "Mod4+q",
|
||||
},
|
||||
{
|
||||
name: "multiple_mods",
|
||||
keybind: &sway.KeyBinding{
|
||||
Mods: []string{"Mod4", "Shift"},
|
||||
Key: "e",
|
||||
},
|
||||
expected: "Mod4+Shift+e",
|
||||
},
|
||||
{
|
||||
name: "no_mods",
|
||||
keybind: &sway.KeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
},
|
||||
expected: "Print",
|
||||
},
|
||||
}
|
||||
|
||||
provider := NewSwayProvider("")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := provider.formatKey(tt.keybind)
|
||||
if result != tt.expected {
|
||||
t.Errorf("formatKey() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayConvertKeybind(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *sway.KeyBinding
|
||||
wantKey string
|
||||
wantDesc string
|
||||
}{
|
||||
{
|
||||
name: "with_comment",
|
||||
keybind: &sway.KeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "t",
|
||||
Command: "exec kitty",
|
||||
Comment: "Open terminal",
|
||||
},
|
||||
wantKey: "Mod4+t",
|
||||
wantDesc: "Open terminal",
|
||||
},
|
||||
{
|
||||
name: "without_comment",
|
||||
keybind: &sway.KeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "r",
|
||||
Command: "reload",
|
||||
Comment: "",
|
||||
},
|
||||
wantKey: "Mod4+r",
|
||||
wantDesc: "reload",
|
||||
},
|
||||
}
|
||||
|
||||
provider := NewSwayProvider("")
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := provider.convertKeybind(tt.keybind, "")
|
||||
if result.Key != tt.wantKey {
|
||||
t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey)
|
||||
}
|
||||
if result.Description != tt.wantDesc {
|
||||
t.Errorf("convertKeybind().Description = %q, want %q", result.Description, tt.wantDesc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayGetCheatSheet(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config")
|
||||
|
||||
content := `set $mod Mod4
|
||||
set $term kitty
|
||||
|
||||
# System
|
||||
bindsym $mod+Shift+c reload
|
||||
bindsym $mod+Shift+e exit
|
||||
|
||||
# Applications
|
||||
bindsym $mod+t exec $term
|
||||
bindsym $mod+Space exec rofi
|
||||
|
||||
# Window Management
|
||||
bindsym $mod+q kill
|
||||
bindsym $mod+f fullscreen toggle
|
||||
bindsym $mod+Left focus left
|
||||
bindsym $mod+Right focus right
|
||||
|
||||
# Workspace
|
||||
bindsym $mod+1 workspace number 1
|
||||
bindsym $mod+2 workspace number 2
|
||||
bindsym $mod+Shift+1 move container to workspace number 1
|
||||
|
||||
# Layout
|
||||
bindsym $mod+s layout stacking
|
||||
bindsym $mod+w layout tabbed
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
provider := NewSwayProvider(tmpDir)
|
||||
sheet, err := provider.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
if sheet == nil {
|
||||
t.Fatal("Expected non-nil CheatSheet")
|
||||
}
|
||||
|
||||
if sheet.Title != "Sway Keybinds" {
|
||||
t.Errorf("Title = %q, want %q", sheet.Title, "Sway Keybinds")
|
||||
}
|
||||
|
||||
if sheet.Provider != "sway" {
|
||||
t.Errorf("Provider = %q, want %q", sheet.Provider, "sway")
|
||||
}
|
||||
|
||||
categories := []string{"System", "Execute", "Window", "Workspace", "Layout"}
|
||||
for _, category := range categories {
|
||||
if _, exists := sheet.Binds[category]; !exists {
|
||||
t.Errorf("Expected category %q to exist", category)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sheet.Binds["System"]) < 2 {
|
||||
t.Error("Expected at least 2 System keybinds")
|
||||
}
|
||||
if len(sheet.Binds["Execute"]) < 2 {
|
||||
t.Error("Expected at least 2 Execute keybinds")
|
||||
}
|
||||
if len(sheet.Binds["Window"]) < 4 {
|
||||
t.Error("Expected at least 4 Window keybinds")
|
||||
}
|
||||
if len(sheet.Binds["Workspace"]) < 3 {
|
||||
t.Error("Expected at least 3 Workspace keybinds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayGetCheatSheetError(t *testing.T) {
|
||||
provider := NewSwayProvider("/nonexistent/path")
|
||||
_, err := provider.GetCheatSheet()
|
||||
if err == nil {
|
||||
t.Error("Expected error for nonexistent path, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayIntegration(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config")
|
||||
|
||||
content := `set $mod Mod4
|
||||
|
||||
bindsym $mod+t exec kitty # Terminal
|
||||
bindsym $mod+q kill
|
||||
bindsym $mod+f fullscreen toggle
|
||||
bindsym $mod+1 workspace number 1
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
provider := NewSwayProvider(tmpDir)
|
||||
sheet, err := provider.GetCheatSheet()
|
||||
if err != nil {
|
||||
t.Fatalf("GetCheatSheet failed: %v", err)
|
||||
}
|
||||
|
||||
totalBinds := 0
|
||||
for _, binds := range sheet.Binds {
|
||||
totalBinds += len(binds)
|
||||
}
|
||||
|
||||
expectedBinds := 4
|
||||
if totalBinds != expectedBinds {
|
||||
t.Errorf("Expected %d total keybinds, got %d", expectedBinds, totalBinds)
|
||||
}
|
||||
|
||||
foundTerminal := false
|
||||
for _, binds := range sheet.Binds {
|
||||
for _, bind := range binds {
|
||||
if bind.Description == "Terminal" && bind.Key == "Mod4+t" {
|
||||
foundTerminal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundTerminal {
|
||||
t.Error("Did not find terminal keybind with correct key and description")
|
||||
}
|
||||
}
|
||||
79
internal/keybinds/registry.go
Normal file
79
internal/keybinds/registry.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package keybinds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
providers map[string]Provider
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
providers: make(map[string]Provider),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) Register(provider Provider) error {
|
||||
if provider == nil {
|
||||
return fmt.Errorf("cannot register nil provider")
|
||||
}
|
||||
|
||||
name := provider.Name()
|
||||
if name == "" {
|
||||
return fmt.Errorf("provider name cannot be empty")
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, exists := r.providers[name]; exists {
|
||||
return fmt.Errorf("provider %q already registered", name)
|
||||
}
|
||||
|
||||
r.providers[name] = provider
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) (Provider, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
provider, exists := r.providers[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("provider %q not found", name)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (r *Registry) List() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
names := make([]string, 0, len(r.providers))
|
||||
for name := range r.providers {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
var defaultRegistry = NewRegistry()
|
||||
|
||||
func GetDefaultRegistry() *Registry {
|
||||
return defaultRegistry
|
||||
}
|
||||
|
||||
func Register(provider Provider) error {
|
||||
return defaultRegistry.Register(provider)
|
||||
}
|
||||
|
||||
func Get(name string) (Provider, error) {
|
||||
return defaultRegistry.Get(name)
|
||||
}
|
||||
|
||||
func List() []string {
|
||||
return defaultRegistry.List()
|
||||
}
|
||||
183
internal/keybinds/registry_test.go
Normal file
183
internal/keybinds/registry_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package keybinds
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockProvider struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *mockProvider) Name() string {
|
||||
return m.name
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetCheatSheet() (*CheatSheet, error) {
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
return &CheatSheet{
|
||||
Title: "Test",
|
||||
Provider: m.name,
|
||||
Binds: make(map[string][]Keybind),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestNewRegistry(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
if r == nil {
|
||||
t.Fatal("NewRegistry returned nil")
|
||||
}
|
||||
|
||||
if r.providers == nil {
|
||||
t.Error("providers map is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
provider Provider
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid provider",
|
||||
provider: &mockProvider{name: "test"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nil provider",
|
||||
provider: nil,
|
||||
expectError: true,
|
||||
errorMsg: "cannot register nil provider",
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
provider: &mockProvider{name: ""},
|
||||
expectError: true,
|
||||
errorMsg: "provider name cannot be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
err := r.Register(tt.provider)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterDuplicate(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
p := &mockProvider{name: "test"}
|
||||
|
||||
if err := r.Register(p); err != nil {
|
||||
t.Fatalf("first registration failed: %v", err)
|
||||
}
|
||||
|
||||
err := r.Register(p)
|
||||
if err == nil {
|
||||
t.Error("expected error when registering duplicate, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProvider(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
p := &mockProvider{name: "test"}
|
||||
|
||||
if err := r.Register(p); err != nil {
|
||||
t.Fatalf("registration failed: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := r.Get("test")
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name() != "test" {
|
||||
t.Errorf("Got provider name %q, want %q", retrieved.Name(), "test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNonexistent(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
_, err := r.Get("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("expected error for nonexistent provider, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProviders(t *testing.T) {
|
||||
r := NewRegistry()
|
||||
|
||||
p1 := &mockProvider{name: "test1"}
|
||||
p2 := &mockProvider{name: "test2"}
|
||||
p3 := &mockProvider{name: "test3"}
|
||||
|
||||
r.Register(p1)
|
||||
r.Register(p2)
|
||||
r.Register(p3)
|
||||
|
||||
list := r.List()
|
||||
|
||||
if len(list) != 3 {
|
||||
t.Errorf("expected 3 providers, got %d", len(list))
|
||||
}
|
||||
|
||||
found := make(map[string]bool)
|
||||
for _, name := range list {
|
||||
found[name] = true
|
||||
}
|
||||
|
||||
expected := []string{"test1", "test2", "test3"}
|
||||
for _, name := range expected {
|
||||
if !found[name] {
|
||||
t.Errorf("expected provider %q not found in list", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRegistry(t *testing.T) {
|
||||
p := &mockProvider{name: "default-test"}
|
||||
|
||||
err := Register(p)
|
||||
if err != nil {
|
||||
t.Fatalf("Register failed: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := Get("default-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name() != "default-test" {
|
||||
t.Errorf("Got provider name %q, want %q", retrieved.Name(), "default-test")
|
||||
}
|
||||
|
||||
list := List()
|
||||
found := false
|
||||
for _, name := range list {
|
||||
if name == "default-test" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("provider not found in default registry list")
|
||||
}
|
||||
}
|
||||
18
internal/keybinds/types.go
Normal file
18
internal/keybinds/types.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package keybinds
|
||||
|
||||
type Keybind struct {
|
||||
Key string `json:"key"`
|
||||
Description string `json:"desc"`
|
||||
Subcategory string `json:"subcat,omitempty"`
|
||||
}
|
||||
|
||||
type CheatSheet struct {
|
||||
Title string `json:"title"`
|
||||
Provider string `json:"provider"`
|
||||
Binds map[string][]Keybind `json:"binds"`
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
Name() string
|
||||
GetCheatSheet() (*CheatSheet, error)
|
||||
}
|
||||
116
internal/log/log.go
Normal file
116
internal/log/log.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
cblog "github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// Logger embeds the Charm Logger and adds Printf/Fatalf
|
||||
type Logger struct{ *cblog.Logger }
|
||||
|
||||
// Printf routes goose/info-style logs through Infof.
|
||||
func (l *Logger) Printf(format string, v ...interface{}) { l.Infof(format, v...) }
|
||||
|
||||
// Fatalf keeps goose’s contract of exiting the program.
|
||||
func (l *Logger) Fatalf(format string, v ...interface{}) { l.Logger.Fatalf(format, v...) }
|
||||
|
||||
var (
|
||||
logger *Logger
|
||||
initLogger sync.Once
|
||||
)
|
||||
|
||||
func parseLogLevel(level string) cblog.Level {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
return cblog.DebugLevel
|
||||
case "info":
|
||||
return cblog.InfoLevel
|
||||
case "warn", "warning":
|
||||
return cblog.WarnLevel
|
||||
case "error":
|
||||
return cblog.ErrorLevel
|
||||
case "fatal":
|
||||
return cblog.FatalLevel
|
||||
default:
|
||||
return cblog.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func GetQtLoggingRules() string {
|
||||
level := os.Getenv("DMS_LOG_LEVEL")
|
||||
if level == "" {
|
||||
level = "info"
|
||||
}
|
||||
|
||||
var rules []string
|
||||
switch strings.ToLower(level) {
|
||||
case "fatal":
|
||||
rules = []string{"*.debug=false", "*.info=false", "*.warning=false", "*.critical=false"}
|
||||
case "error":
|
||||
rules = []string{"*.debug=false", "*.info=false", "*.warning=false"}
|
||||
case "warn", "warning":
|
||||
rules = []string{"*.debug=false", "*.info=false"}
|
||||
case "info":
|
||||
rules = []string{"*.debug=false"}
|
||||
case "debug":
|
||||
return ""
|
||||
default:
|
||||
rules = []string{"*.debug=false"}
|
||||
}
|
||||
|
||||
return strings.Join(rules, ";")
|
||||
}
|
||||
|
||||
// GetLogger returns a logger instance
|
||||
func GetLogger() *Logger {
|
||||
initLogger.Do(func() {
|
||||
styles := cblog.DefaultStyles()
|
||||
// Attempt to match the colors used by qml/quickshell logs
|
||||
styles.Levels[cblog.FatalLevel] = lipgloss.NewStyle().
|
||||
SetString(" FATAL").
|
||||
Foreground(lipgloss.Color("1"))
|
||||
styles.Levels[cblog.ErrorLevel] = lipgloss.NewStyle().
|
||||
SetString(" ERROR").
|
||||
Foreground(lipgloss.Color("9"))
|
||||
styles.Levels[cblog.WarnLevel] = lipgloss.NewStyle().
|
||||
SetString(" WARN").
|
||||
Foreground(lipgloss.Color("3"))
|
||||
styles.Levels[cblog.InfoLevel] = lipgloss.NewStyle().
|
||||
SetString(" INFO").
|
||||
Foreground(lipgloss.Color("2"))
|
||||
styles.Levels[cblog.DebugLevel] = lipgloss.NewStyle().
|
||||
SetString(" DEBUG").
|
||||
Foreground(lipgloss.Color("4"))
|
||||
|
||||
base := cblog.New(os.Stderr)
|
||||
base.SetStyles(styles)
|
||||
base.SetReportTimestamp(false)
|
||||
|
||||
level := cblog.InfoLevel
|
||||
if envLevel := os.Getenv("DMS_LOG_LEVEL"); envLevel != "" {
|
||||
level = parseLogLevel(envLevel)
|
||||
}
|
||||
base.SetLevel(level)
|
||||
base.SetPrefix(" go")
|
||||
|
||||
logger = &Logger{base}
|
||||
})
|
||||
return logger
|
||||
}
|
||||
|
||||
// * Convenience wrappers
|
||||
|
||||
func Debug(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Debug(msg, keyvals...) }
|
||||
func Debugf(format string, v ...interface{}) { GetLogger().Logger.Debugf(format, v...) }
|
||||
func Info(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Info(msg, keyvals...) }
|
||||
func Infof(format string, v ...interface{}) { GetLogger().Logger.Infof(format, v...) }
|
||||
func Warn(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Warn(msg, keyvals...) }
|
||||
func Warnf(format string, v ...interface{}) { GetLogger().Logger.Warnf(format, v...) }
|
||||
func Error(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Error(msg, keyvals...) }
|
||||
func Errorf(format string, v ...interface{}) { GetLogger().Logger.Errorf(format, v...) }
|
||||
func Fatal(msg interface{}, keyvals ...interface{}) { GetLogger().Logger.Fatal(msg, keyvals...) }
|
||||
func Fatalf(format string, v ...interface{}) { GetLogger().Logger.Fatalf(format, v...) }
|
||||
305
internal/mangowc/keybinds.go
Normal file
305
internal/mangowc/keybinds.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package mangowc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
HideComment = "[hidden]"
|
||||
)
|
||||
|
||||
var ModSeparators = []rune{'+', ' '}
|
||||
|
||||
type KeyBinding struct {
|
||||
Mods []string `json:"mods"`
|
||||
Key string `json:"key"`
|
||||
Command string `json:"command"`
|
||||
Params string `json:"params"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
contentLines []string
|
||||
readingLine int
|
||||
}
|
||||
|
||||
func NewParser() *Parser {
|
||||
return &Parser{
|
||||
contentLines: []string{},
|
||||
readingLine: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ReadContent(path string) error {
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
expandedPath = filepath.Clean(expandedPath)
|
||||
if strings.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
info, err := os.Stat(expandedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var files []string
|
||||
if info.IsDir() {
|
||||
confFiles, err := filepath.Glob(filepath.Join(expandedPath, "*.conf"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(confFiles) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
files = confFiles
|
||||
} else {
|
||||
files = []string{expandedPath}
|
||||
}
|
||||
|
||||
var combinedContent []string
|
||||
for _, file := range files {
|
||||
if fileInfo, err := os.Stat(file); err == nil && fileInfo.Mode().IsRegular() {
|
||||
data, err := os.ReadFile(file)
|
||||
if err == nil {
|
||||
combinedContent = append(combinedContent, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(combinedContent) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fullContent := strings.Join(combinedContent, "\n")
|
||||
p.contentLines = strings.Split(fullContent, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func autogenerateComment(command, params string) string {
|
||||
switch command {
|
||||
case "spawn", "spawn_shell":
|
||||
return params
|
||||
case "killclient":
|
||||
return "Close window"
|
||||
case "quit":
|
||||
return "Exit MangoWC"
|
||||
case "reload_config":
|
||||
return "Reload configuration"
|
||||
case "focusstack":
|
||||
if params == "next" {
|
||||
return "Focus next window"
|
||||
}
|
||||
if params == "prev" {
|
||||
return "Focus previous window"
|
||||
}
|
||||
return "Focus stack " + params
|
||||
case "focusdir":
|
||||
dirMap := map[string]string{
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"up": "up",
|
||||
"down": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "Focus " + dir
|
||||
}
|
||||
return "Focus " + params
|
||||
case "exchange_client":
|
||||
dirMap := map[string]string{
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"up": "up",
|
||||
"down": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "Swap window " + dir
|
||||
}
|
||||
return "Swap window " + params
|
||||
case "togglefloating":
|
||||
return "Float/unfloat window"
|
||||
case "togglefullscreen":
|
||||
return "Toggle fullscreen"
|
||||
case "togglefakefullscreen":
|
||||
return "Toggle fake fullscreen"
|
||||
case "togglemaximizescreen":
|
||||
return "Toggle maximize"
|
||||
case "toggleglobal":
|
||||
return "Toggle global"
|
||||
case "toggleoverview":
|
||||
return "Toggle overview"
|
||||
case "toggleoverlay":
|
||||
return "Toggle overlay"
|
||||
case "minimized":
|
||||
return "Minimize window"
|
||||
case "restore_minimized":
|
||||
return "Restore minimized"
|
||||
case "toggle_scratchpad":
|
||||
return "Toggle scratchpad"
|
||||
case "setlayout":
|
||||
return "Set layout " + params
|
||||
case "switch_layout":
|
||||
return "Switch layout"
|
||||
case "view":
|
||||
parts := strings.Split(params, ",")
|
||||
if len(parts) > 0 {
|
||||
return "View tag " + parts[0]
|
||||
}
|
||||
return "View tag"
|
||||
case "tag":
|
||||
parts := strings.Split(params, ",")
|
||||
if len(parts) > 0 {
|
||||
return "Move to tag " + parts[0]
|
||||
}
|
||||
return "Move to tag"
|
||||
case "toggleview":
|
||||
parts := strings.Split(params, ",")
|
||||
if len(parts) > 0 {
|
||||
return "Toggle tag " + parts[0]
|
||||
}
|
||||
return "Toggle tag"
|
||||
case "viewtoleft", "viewtoleft_have_client":
|
||||
return "View left tag"
|
||||
case "viewtoright", "viewtoright_have_client":
|
||||
return "View right tag"
|
||||
case "tagtoleft":
|
||||
return "Move to left tag"
|
||||
case "tagtoright":
|
||||
return "Move to right tag"
|
||||
case "focusmon":
|
||||
return "Focus monitor " + params
|
||||
case "tagmon":
|
||||
return "Move to monitor " + params
|
||||
case "incgaps":
|
||||
if strings.HasPrefix(params, "-") {
|
||||
return "Decrease gaps"
|
||||
}
|
||||
return "Increase gaps"
|
||||
case "togglegaps":
|
||||
return "Toggle gaps"
|
||||
case "movewin":
|
||||
return "Move window by " + params
|
||||
case "resizewin":
|
||||
return "Resize window by " + params
|
||||
case "set_proportion":
|
||||
return "Set proportion " + params
|
||||
case "switch_proportion_preset":
|
||||
return "Switch proportion preset"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
|
||||
if lineNumber >= len(p.contentLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := p.contentLines[lineNumber]
|
||||
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindType := matches[1]
|
||||
content := matches[2]
|
||||
|
||||
parts := strings.SplitN(content, "#", 2)
|
||||
keys := parts[0]
|
||||
|
||||
var comment string
|
||||
if len(parts) > 1 {
|
||||
comment = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(comment, HideComment) {
|
||||
return nil
|
||||
}
|
||||
|
||||
keyFields := strings.SplitN(keys, ",", 4)
|
||||
if len(keyFields) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(keyFields[0])
|
||||
key := strings.TrimSpace(keyFields[1])
|
||||
command := strings.TrimSpace(keyFields[2])
|
||||
|
||||
var params string
|
||||
if len(keyFields) > 3 {
|
||||
params = strings.TrimSpace(keyFields[3])
|
||||
}
|
||||
|
||||
if comment == "" {
|
||||
comment = autogenerateComment(command, params)
|
||||
}
|
||||
|
||||
var modList []string
|
||||
if mods != "" && !strings.EqualFold(mods, "none") {
|
||||
modstring := mods + string(ModSeparators[0])
|
||||
p := 0
|
||||
for index, char := range modstring {
|
||||
isModSep := false
|
||||
for _, sep := range ModSeparators {
|
||||
if char == sep {
|
||||
isModSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isModSep {
|
||||
if index-p > 1 {
|
||||
modList = append(modList, modstring[p:index])
|
||||
}
|
||||
p = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = bindType
|
||||
|
||||
return &KeyBinding{
|
||||
Mods: modList,
|
||||
Key: key,
|
||||
Command: command,
|
||||
Params: params,
|
||||
Comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) ParseKeys() []KeyBinding {
|
||||
var keybinds []KeyBinding
|
||||
|
||||
for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ {
|
||||
line := p.contentLines[lineNumber]
|
||||
if line == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(strings.TrimSpace(line), "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
keybind := p.getKeybindAtLine(lineNumber)
|
||||
if keybind != nil {
|
||||
keybinds = append(keybinds, *keybind)
|
||||
}
|
||||
}
|
||||
|
||||
return keybinds
|
||||
}
|
||||
|
||||
func ParseKeys(path string) ([]KeyBinding, error) {
|
||||
parser := NewParser()
|
||||
if err := parser.ReadContent(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parser.ParseKeys(), nil
|
||||
}
|
||||
499
internal/mangowc/keybinds_test.go
Normal file
499
internal/mangowc/keybinds_test.go
Normal file
@@ -0,0 +1,499 @@
|
||||
package mangowc
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAutogenerateComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
command string
|
||||
params string
|
||||
expected string
|
||||
}{
|
||||
{"spawn", "kitty", "kitty"},
|
||||
{"spawn_shell", "firefox", "firefox"},
|
||||
{"killclient", "", "Close window"},
|
||||
{"quit", "", "Exit MangoWC"},
|
||||
{"reload_config", "", "Reload configuration"},
|
||||
{"focusstack", "next", "Focus next window"},
|
||||
{"focusstack", "prev", "Focus previous window"},
|
||||
{"focusdir", "left", "Focus left"},
|
||||
{"focusdir", "right", "Focus right"},
|
||||
{"focusdir", "up", "Focus up"},
|
||||
{"focusdir", "down", "Focus down"},
|
||||
{"exchange_client", "left", "Swap window left"},
|
||||
{"exchange_client", "right", "Swap window right"},
|
||||
{"togglefloating", "", "Float/unfloat window"},
|
||||
{"togglefullscreen", "", "Toggle fullscreen"},
|
||||
{"togglefakefullscreen", "", "Toggle fake fullscreen"},
|
||||
{"togglemaximizescreen", "", "Toggle maximize"},
|
||||
{"toggleglobal", "", "Toggle global"},
|
||||
{"toggleoverview", "", "Toggle overview"},
|
||||
{"toggleoverlay", "", "Toggle overlay"},
|
||||
{"minimized", "", "Minimize window"},
|
||||
{"restore_minimized", "", "Restore minimized"},
|
||||
{"toggle_scratchpad", "", "Toggle scratchpad"},
|
||||
{"setlayout", "tile", "Set layout tile"},
|
||||
{"switch_layout", "", "Switch layout"},
|
||||
{"view", "1,0", "View tag 1"},
|
||||
{"tag", "2,0", "Move to tag 2"},
|
||||
{"toggleview", "3,0", "Toggle tag 3"},
|
||||
{"viewtoleft", "", "View left tag"},
|
||||
{"viewtoright", "", "View right tag"},
|
||||
{"viewtoleft_have_client", "", "View left tag"},
|
||||
{"viewtoright_have_client", "", "View right tag"},
|
||||
{"tagtoleft", "", "Move to left tag"},
|
||||
{"tagtoright", "", "Move to right tag"},
|
||||
{"focusmon", "left", "Focus monitor left"},
|
||||
{"tagmon", "right", "Move to monitor right"},
|
||||
{"incgaps", "1", "Increase gaps"},
|
||||
{"incgaps", "-1", "Decrease gaps"},
|
||||
{"togglegaps", "", "Toggle gaps"},
|
||||
{"movewin", "+0,-50", "Move window by +0,-50"},
|
||||
{"resizewin", "+0,+50", "Resize window by +0,+50"},
|
||||
{"set_proportion", "1.0", "Set proportion 1.0"},
|
||||
{"switch_proportion_preset", "", "Switch proportion preset"},
|
||||
{"unknown", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.command+"_"+tt.params, func(t *testing.T) {
|
||||
result := autogenerateComment(tt.command, tt.params)
|
||||
if result != tt.expected {
|
||||
t.Errorf("autogenerateComment(%q, %q) = %q, want %q",
|
||||
tt.command, tt.params, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *KeyBinding
|
||||
}{
|
||||
{
|
||||
name: "basic_keybind",
|
||||
line: "bind=ALT,q,killclient,",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "q",
|
||||
Command: "killclient",
|
||||
Params: "",
|
||||
Comment: "Close window",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_params",
|
||||
line: "bind=ALT,Left,focusdir,left",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "Left",
|
||||
Command: "focusdir",
|
||||
Params: "left",
|
||||
Comment: "Focus left",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_comment",
|
||||
line: "bind=Alt,t,spawn,kitty # Open terminal",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"Alt"},
|
||||
Key: "t",
|
||||
Command: "spawn",
|
||||
Params: "kitty",
|
||||
Comment: "Open terminal",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_hidden",
|
||||
line: "bind=SUPER,h,spawn,secret # [hidden]",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_mods",
|
||||
line: "bind=SUPER+SHIFT,Up,exchange_client,up",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "Up",
|
||||
Command: "exchange_client",
|
||||
Params: "up",
|
||||
Comment: "Swap window up",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_no_mods",
|
||||
line: "bind=NONE,Print,spawn,screenshot",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
Command: "spawn",
|
||||
Params: "screenshot",
|
||||
Comment: "screenshot",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_params",
|
||||
line: "bind=Ctrl,1,view,1,0",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"Ctrl"},
|
||||
Key: "1",
|
||||
Command: "view",
|
||||
Params: "1,0",
|
||||
Comment: "View tag 1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bindl_flag",
|
||||
line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER", "ALT"},
|
||||
Key: "l",
|
||||
Command: "spawn",
|
||||
Params: "dms ipc call lock lock",
|
||||
Comment: "dms ipc call lock lock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_spaces",
|
||||
line: "bind = SUPER, r, reload_config",
|
||||
expected: &KeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "r",
|
||||
Command: "reload_config",
|
||||
Params: "",
|
||||
Comment: "Reload configuration",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Errorf("expected nil, got %+v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Errorf("expected %+v, got nil", tt.expected)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Key != tt.expected.Key {
|
||||
t.Errorf("Key = %q, want %q", result.Key, tt.expected.Key)
|
||||
}
|
||||
if result.Command != tt.expected.Command {
|
||||
t.Errorf("Command = %q, want %q", result.Command, tt.expected.Command)
|
||||
}
|
||||
if result.Params != tt.expected.Params {
|
||||
t.Errorf("Params = %q, want %q", result.Params, tt.expected.Params)
|
||||
}
|
||||
if result.Comment != tt.expected.Comment {
|
||||
t.Errorf("Comment = %q, want %q", result.Comment, tt.expected.Comment)
|
||||
}
|
||||
if len(result.Mods) != len(tt.expected.Mods) {
|
||||
t.Errorf("Mods length = %d, want %d", len(result.Mods), len(tt.expected.Mods))
|
||||
} else {
|
||||
for i := range result.Mods {
|
||||
if result.Mods[i] != tt.expected.Mods[i] {
|
||||
t.Errorf("Mods[%d] = %q, want %q", i, result.Mods[i], tt.expected.Mods[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeys(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `# MangoWC Configuration
|
||||
blur=0
|
||||
border_radius=12
|
||||
|
||||
# Key Bindings
|
||||
bind=SUPER,r,reload_config
|
||||
bind=Alt,t,spawn,kitty # Terminal
|
||||
bind=ALT,q,killclient,
|
||||
bind=ALT,Left,focusdir,left
|
||||
|
||||
# Hidden binding
|
||||
bind=SUPER,h,spawn,secret # [hidden]
|
||||
|
||||
# Multiple modifiers
|
||||
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||
|
||||
# Workspace bindings
|
||||
bind=Ctrl,1,view,1,0
|
||||
bind=Ctrl,2,view,2,0
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
keybinds, err := ParseKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKeys failed: %v", err)
|
||||
}
|
||||
|
||||
expectedCount := 7
|
||||
if len(keybinds) != expectedCount {
|
||||
t.Errorf("Expected %d keybinds, got %d", expectedCount, len(keybinds))
|
||||
}
|
||||
|
||||
if len(keybinds) > 0 && keybinds[0].Command != "reload_config" {
|
||||
t.Errorf("First keybind command = %q, want %q", keybinds[0].Command, "reload_config")
|
||||
}
|
||||
|
||||
foundHidden := false
|
||||
for _, kb := range keybinds {
|
||||
if kb.Command == "spawn" && kb.Params == "secret" {
|
||||
foundHidden = true
|
||||
}
|
||||
}
|
||||
if foundHidden {
|
||||
t.Error("Hidden keybind should not be included in results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentMultipleFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file1 := filepath.Join(tmpDir, "a.conf")
|
||||
file2 := filepath.Join(tmpDir, "b.conf")
|
||||
|
||||
content1 := "bind=ALT,q,killclient,\n"
|
||||
content2 := "bind=Alt,t,spawn,kitty\n"
|
||||
|
||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file1: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file2: %v", err)
|
||||
}
|
||||
|
||||
parser := NewParser()
|
||||
if err := parser.ReadContent(tmpDir); err != nil {
|
||||
t.Fatalf("ReadContent failed: %v", err)
|
||||
}
|
||||
|
||||
keybinds := parser.ParseKeys()
|
||||
if len(keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds from multiple files, got %d", len(keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentSingleFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := "bind=ALT,q,killclient,\n"
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
parser := NewParser()
|
||||
if err := parser.ReadContent(configFile); err != nil {
|
||||
t.Fatalf("ReadContent failed: %v", err)
|
||||
}
|
||||
|
||||
keybinds := parser.ParseKeys()
|
||||
if len(keybinds) != 1 {
|
||||
t.Errorf("Expected 1 keybind, got %d", len(keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "nonexistent_directory",
|
||||
path: "/nonexistent/path/that/does/not/exist",
|
||||
},
|
||||
{
|
||||
name: "empty_directory",
|
||||
path: t.TempDir(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseKeys(tt.path)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadContentWithTildeExpansion(t *testing.T) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||
t.Skip("Cannot create test directory in home")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
configFile := filepath.Join(tmpSubdir, "config.conf")
|
||||
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(homeDir, tmpSubdir)
|
||||
if err != nil {
|
||||
t.Skip("Cannot create relative path")
|
||||
}
|
||||
|
||||
parser := NewParser()
|
||||
tildePathMatch := "~/" + relPath
|
||||
err = parser.ReadContent(tildePathMatch)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ReadContent with tilde path failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyAndCommentLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `
|
||||
# This is a comment
|
||||
bind=ALT,q,killclient,
|
||||
|
||||
# Another comment
|
||||
|
||||
bind=Alt,t,spawn,kitty
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
keybinds, err := ParseKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidBindLines(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
}{
|
||||
{
|
||||
name: "missing_parts",
|
||||
line: "bind=SUPER,q",
|
||||
},
|
||||
{
|
||||
name: "not_bind",
|
||||
line: "blur=0",
|
||||
},
|
||||
{
|
||||
name: "empty_line",
|
||||
line: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil for invalid line, got %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealWorldConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `# Application Launchers
|
||||
bind=Alt,t,spawn,kitty
|
||||
bind=Alt,space,spawn,dms ipc call spotlight toggle
|
||||
bind=Alt,v,spawn,dms ipc call clipboard toggle
|
||||
|
||||
# exit
|
||||
bind=ALT+SHIFT,e,quit
|
||||
bind=ALT,q,killclient,
|
||||
|
||||
# switch window focus
|
||||
bind=SUPER,Tab,focusstack,next
|
||||
bind=ALT,Left,focusdir,left
|
||||
bind=ALT,Right,focusdir,right
|
||||
|
||||
# tag switch
|
||||
bind=SUPER,Left,viewtoleft,0
|
||||
bind=CTRL,Left,viewtoleft_have_client,0
|
||||
bind=SUPER,Right,viewtoright,0
|
||||
|
||||
bind=Ctrl,1,view,1,0
|
||||
bind=Ctrl,2,view,2,0
|
||||
bind=Ctrl,3,view,3,0
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
keybinds, err := ParseKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(keybinds) < 14 {
|
||||
t.Errorf("Expected at least 14 keybinds, got %d", len(keybinds))
|
||||
}
|
||||
|
||||
foundSpawn := false
|
||||
foundQuit := false
|
||||
foundView := false
|
||||
|
||||
for _, kb := range keybinds {
|
||||
if kb.Command == "spawn" && kb.Params == "kitty" {
|
||||
foundSpawn = true
|
||||
}
|
||||
if kb.Command == "quit" {
|
||||
foundQuit = true
|
||||
}
|
||||
if kb.Command == "view" && kb.Params == "1,0" {
|
||||
foundView = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundSpawn {
|
||||
t.Error("Did not find spawn kitty keybind")
|
||||
}
|
||||
if !foundQuit {
|
||||
t.Error("Did not find quit keybind")
|
||||
}
|
||||
if !foundView {
|
||||
t.Error("Did not find view workspace 1 keybind")
|
||||
}
|
||||
}
|
||||
129
internal/mocks/brightness/mock_DBusConn.go
Normal file
129
internal/mocks/brightness/mock_DBusConn.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package mocks_brightness
|
||||
|
||||
import (
|
||||
dbus "github.com/godbus/dbus/v5"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockDBusConn is an autogenerated mock type for the DBusConn type
|
||||
type MockDBusConn struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockDBusConn_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockDBusConn) EXPECT() *MockDBusConn_Expecter {
|
||||
return &MockDBusConn_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with no fields
|
||||
func (_m *MockDBusConn) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockDBusConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockDBusConn_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockDBusConn_Expecter) Close() *MockDBusConn_Close_Call {
|
||||
return &MockDBusConn_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockDBusConn_Close_Call) Run(run func()) *MockDBusConn_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockDBusConn_Close_Call) Return(_a0 error) *MockDBusConn_Close_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockDBusConn_Close_Call) RunAndReturn(run func() error) *MockDBusConn_Close_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Object provides a mock function with given fields: dest, path
|
||||
func (_m *MockDBusConn) Object(dest string, path dbus.ObjectPath) dbus.BusObject {
|
||||
ret := _m.Called(dest, path)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Object")
|
||||
}
|
||||
|
||||
var r0 dbus.BusObject
|
||||
if rf, ok := ret.Get(0).(func(string, dbus.ObjectPath) dbus.BusObject); ok {
|
||||
r0 = rf(dest, path)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(dbus.BusObject)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockDBusConn_Object_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Object'
|
||||
type MockDBusConn_Object_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Object is a helper method to define mock.On call
|
||||
// - dest string
|
||||
// - path dbus.ObjectPath
|
||||
func (_e *MockDBusConn_Expecter) Object(dest interface{}, path interface{}) *MockDBusConn_Object_Call {
|
||||
return &MockDBusConn_Object_Call{Call: _e.mock.On("Object", dest, path)}
|
||||
}
|
||||
|
||||
func (_c *MockDBusConn_Object_Call) Run(run func(dest string, path dbus.ObjectPath)) *MockDBusConn_Object_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(dbus.ObjectPath))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockDBusConn_Object_Call) Return(_a0 dbus.BusObject) *MockDBusConn_Object_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockDBusConn_Object_Call) RunAndReturn(run func(string, dbus.ObjectPath) dbus.BusObject) *MockDBusConn_Object_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockDBusConn creates a new instance of MockDBusConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockDBusConn(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockDBusConn {
|
||||
mock := &MockDBusConn{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
405
internal/mocks/cups/mock_CUPSClientInterface.go
Normal file
405
internal/mocks/cups/mock_CUPSClientInterface.go
Normal file
@@ -0,0 +1,405 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package mocks_cups
|
||||
|
||||
import (
|
||||
io "io"
|
||||
|
||||
ipp "github.com/AvengeMedia/danklinux/pkg/ipp"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockCUPSClientInterface is an autogenerated mock type for the CUPSClientInterface type
|
||||
type MockCUPSClientInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockCUPSClientInterface_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockCUPSClientInterface) EXPECT() *MockCUPSClientInterface_Expecter {
|
||||
return &MockCUPSClientInterface_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// CancelAllJob provides a mock function with given fields: printer, purge
|
||||
func (_m *MockCUPSClientInterface) CancelAllJob(printer string, purge bool) error {
|
||||
ret := _m.Called(printer, purge)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CancelAllJob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, bool) error); ok {
|
||||
r0 = rf(printer, purge)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_CancelAllJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelAllJob'
|
||||
type MockCUPSClientInterface_CancelAllJob_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CancelAllJob is a helper method to define mock.On call
|
||||
// - printer string
|
||||
// - purge bool
|
||||
func (_e *MockCUPSClientInterface_Expecter) CancelAllJob(printer interface{}, purge interface{}) *MockCUPSClientInterface_CancelAllJob_Call {
|
||||
return &MockCUPSClientInterface_CancelAllJob_Call{Call: _e.mock.On("CancelAllJob", printer, purge)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_CancelAllJob_Call) Run(run func(printer string, purge bool)) *MockCUPSClientInterface_CancelAllJob_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(bool))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_CancelAllJob_Call) Return(_a0 error) *MockCUPSClientInterface_CancelAllJob_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_CancelAllJob_Call) RunAndReturn(run func(string, bool) error) *MockCUPSClientInterface_CancelAllJob_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// CancelJob provides a mock function with given fields: jobID, purge
|
||||
func (_m *MockCUPSClientInterface) CancelJob(jobID int, purge bool) error {
|
||||
ret := _m.Called(jobID, purge)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CancelJob")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int, bool) error); ok {
|
||||
r0 = rf(jobID, purge)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_CancelJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelJob'
|
||||
type MockCUPSClientInterface_CancelJob_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CancelJob is a helper method to define mock.On call
|
||||
// - jobID int
|
||||
// - purge bool
|
||||
func (_e *MockCUPSClientInterface_Expecter) CancelJob(jobID interface{}, purge interface{}) *MockCUPSClientInterface_CancelJob_Call {
|
||||
return &MockCUPSClientInterface_CancelJob_Call{Call: _e.mock.On("CancelJob", jobID, purge)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_CancelJob_Call) Run(run func(jobID int, purge bool)) *MockCUPSClientInterface_CancelJob_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(int), args[1].(bool))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_CancelJob_Call) Return(_a0 error) *MockCUPSClientInterface_CancelJob_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_CancelJob_Call) RunAndReturn(run func(int, bool) error) *MockCUPSClientInterface_CancelJob_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetJobs provides a mock function with given fields: printer, class, whichJobs, myJobs, firstJobId, limit, attributes
|
||||
func (_m *MockCUPSClientInterface) GetJobs(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string) (map[int]ipp.Attributes, error) {
|
||||
ret := _m.Called(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetJobs")
|
||||
}
|
||||
|
||||
var r0 map[int]ipp.Attributes
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, bool, int, int, []string) (map[int]ipp.Attributes, error)); ok {
|
||||
return rf(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, bool, int, int, []string) map[int]ipp.Attributes); ok {
|
||||
r0 = rf(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[int]ipp.Attributes)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, string, bool, int, int, []string) error); ok {
|
||||
r1 = rf(printer, class, whichJobs, myJobs, firstJobId, limit, attributes)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_GetJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobs'
|
||||
type MockCUPSClientInterface_GetJobs_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetJobs is a helper method to define mock.On call
|
||||
// - printer string
|
||||
// - class string
|
||||
// - whichJobs string
|
||||
// - myJobs bool
|
||||
// - firstJobId int
|
||||
// - limit int
|
||||
// - attributes []string
|
||||
func (_e *MockCUPSClientInterface_Expecter) GetJobs(printer interface{}, class interface{}, whichJobs interface{}, myJobs interface{}, firstJobId interface{}, limit interface{}, attributes interface{}) *MockCUPSClientInterface_GetJobs_Call {
|
||||
return &MockCUPSClientInterface_GetJobs_Call{Call: _e.mock.On("GetJobs", printer, class, whichJobs, myJobs, firstJobId, limit, attributes)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_GetJobs_Call) Run(run func(printer string, class string, whichJobs string, myJobs bool, firstJobId int, limit int, attributes []string)) *MockCUPSClientInterface_GetJobs_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string), args[2].(string), args[3].(bool), args[4].(int), args[5].(int), args[6].([]string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_GetJobs_Call) Return(_a0 map[int]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetJobs_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_GetJobs_Call) RunAndReturn(run func(string, string, string, bool, int, int, []string) (map[int]ipp.Attributes, error)) *MockCUPSClientInterface_GetJobs_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPrinters provides a mock function with given fields: attributes
|
||||
func (_m *MockCUPSClientInterface) GetPrinters(attributes []string) (map[string]ipp.Attributes, error) {
|
||||
ret := _m.Called(attributes)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPrinters")
|
||||
}
|
||||
|
||||
var r0 map[string]ipp.Attributes
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]string) (map[string]ipp.Attributes, error)); ok {
|
||||
return rf(attributes)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func([]string) map[string]ipp.Attributes); ok {
|
||||
r0 = rf(attributes)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]ipp.Attributes)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func([]string) error); ok {
|
||||
r1 = rf(attributes)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_GetPrinters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrinters'
|
||||
type MockCUPSClientInterface_GetPrinters_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPrinters is a helper method to define mock.On call
|
||||
// - attributes []string
|
||||
func (_e *MockCUPSClientInterface_Expecter) GetPrinters(attributes interface{}) *MockCUPSClientInterface_GetPrinters_Call {
|
||||
return &MockCUPSClientInterface_GetPrinters_Call{Call: _e.mock.On("GetPrinters", attributes)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_GetPrinters_Call) Run(run func(attributes []string)) *MockCUPSClientInterface_GetPrinters_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].([]string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_GetPrinters_Call) Return(_a0 map[string]ipp.Attributes, _a1 error) *MockCUPSClientInterface_GetPrinters_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_GetPrinters_Call) RunAndReturn(run func([]string) (map[string]ipp.Attributes, error)) *MockCUPSClientInterface_GetPrinters_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// PausePrinter provides a mock function with given fields: printer
|
||||
func (_m *MockCUPSClientInterface) PausePrinter(printer string) error {
|
||||
ret := _m.Called(printer)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PausePrinter")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(printer)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_PausePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PausePrinter'
|
||||
type MockCUPSClientInterface_PausePrinter_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// PausePrinter is a helper method to define mock.On call
|
||||
// - printer string
|
||||
func (_e *MockCUPSClientInterface_Expecter) PausePrinter(printer interface{}) *MockCUPSClientInterface_PausePrinter_Call {
|
||||
return &MockCUPSClientInterface_PausePrinter_Call{Call: _e.mock.On("PausePrinter", printer)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_PausePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_PausePrinter_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_PausePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_PausePrinter_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_PausePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_PausePrinter_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ResumePrinter provides a mock function with given fields: printer
|
||||
func (_m *MockCUPSClientInterface) ResumePrinter(printer string) error {
|
||||
ret := _m.Called(printer)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ResumePrinter")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(printer)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_ResumePrinter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResumePrinter'
|
||||
type MockCUPSClientInterface_ResumePrinter_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ResumePrinter is a helper method to define mock.On call
|
||||
// - printer string
|
||||
func (_e *MockCUPSClientInterface_Expecter) ResumePrinter(printer interface{}) *MockCUPSClientInterface_ResumePrinter_Call {
|
||||
return &MockCUPSClientInterface_ResumePrinter_Call{Call: _e.mock.On("ResumePrinter", printer)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_ResumePrinter_Call) Run(run func(printer string)) *MockCUPSClientInterface_ResumePrinter_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_ResumePrinter_Call) Return(_a0 error) *MockCUPSClientInterface_ResumePrinter_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_ResumePrinter_Call) RunAndReturn(run func(string) error) *MockCUPSClientInterface_ResumePrinter_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SendRequest provides a mock function with given fields: url, req, additionalResponseData
|
||||
func (_m *MockCUPSClientInterface) SendRequest(url string, req *ipp.Request, additionalResponseData io.Writer) (*ipp.Response, error) {
|
||||
ret := _m.Called(url, req, additionalResponseData)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SendRequest")
|
||||
}
|
||||
|
||||
var r0 *ipp.Response
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, *ipp.Request, io.Writer) (*ipp.Response, error)); ok {
|
||||
return rf(url, req, additionalResponseData)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, *ipp.Request, io.Writer) *ipp.Response); ok {
|
||||
r0 = rf(url, req, additionalResponseData)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ipp.Response)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, *ipp.Request, io.Writer) error); ok {
|
||||
r1 = rf(url, req, additionalResponseData)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCUPSClientInterface_SendRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendRequest'
|
||||
type MockCUPSClientInterface_SendRequest_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SendRequest is a helper method to define mock.On call
|
||||
// - url string
|
||||
// - req *ipp.Request
|
||||
// - additionalResponseData io.Writer
|
||||
func (_e *MockCUPSClientInterface_Expecter) SendRequest(url interface{}, req interface{}, additionalResponseData interface{}) *MockCUPSClientInterface_SendRequest_Call {
|
||||
return &MockCUPSClientInterface_SendRequest_Call{Call: _e.mock.On("SendRequest", url, req, additionalResponseData)}
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_SendRequest_Call) Run(run func(url string, req *ipp.Request, additionalResponseData io.Writer)) *MockCUPSClientInterface_SendRequest_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(*ipp.Request), args[2].(io.Writer))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_SendRequest_Call) Return(_a0 *ipp.Response, _a1 error) *MockCUPSClientInterface_SendRequest_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCUPSClientInterface_SendRequest_Call) RunAndReturn(run func(string, *ipp.Request, io.Writer) (*ipp.Response, error)) *MockCUPSClientInterface_SendRequest_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockCUPSClientInterface creates a new instance of MockCUPSClientInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockCUPSClientInterface(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockCUPSClientInterface {
|
||||
mock := &MockCUPSClientInterface{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -0,0 +1,689 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package gonetworkmanager
|
||||
|
||||
import (
|
||||
gonetworkmanager "github.com/Wifx/gonetworkmanager/v2"
|
||||
dbus "github.com/godbus/dbus/v5"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockAccessPoint is an autogenerated mock type for the AccessPoint type
|
||||
type MockAccessPoint struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockAccessPoint_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockAccessPoint) EXPECT() *MockAccessPoint_Expecter {
|
||||
return &MockAccessPoint_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetPath provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPath() dbus.ObjectPath {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPath")
|
||||
}
|
||||
|
||||
var r0 dbus.ObjectPath
|
||||
if rf, ok := ret.Get(0).(func() dbus.ObjectPath); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPath'
|
||||
type MockAccessPoint_GetPath_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPath is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPath() *MockAccessPoint_GetPath_Call {
|
||||
return &MockAccessPoint_GetPath_Call{Call: _e.mock.On("GetPath")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPath_Call) Run(run func()) *MockAccessPoint_GetPath_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPath_Call) Return(_a0 dbus.ObjectPath) *MockAccessPoint_GetPath_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPath_Call) RunAndReturn(run func() dbus.ObjectPath) *MockAccessPoint_GetPath_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyFlags provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyFlags() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyFlags")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyFlags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyFlags'
|
||||
type MockAccessPoint_GetPropertyFlags_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyFlags is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyFlags() *MockAccessPoint_GetPropertyFlags_Call {
|
||||
return &MockAccessPoint_GetPropertyFlags_Call{Call: _e.mock.On("GetPropertyFlags")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyFlags_Call) Run(run func()) *MockAccessPoint_GetPropertyFlags_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyFlags_Call) Return(_a0 uint32, _a1 error) *MockAccessPoint_GetPropertyFlags_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyFlags_Call) RunAndReturn(run func() (uint32, error)) *MockAccessPoint_GetPropertyFlags_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyFrequency provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyFrequency() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyFrequency")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyFrequency_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyFrequency'
|
||||
type MockAccessPoint_GetPropertyFrequency_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyFrequency is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyFrequency() *MockAccessPoint_GetPropertyFrequency_Call {
|
||||
return &MockAccessPoint_GetPropertyFrequency_Call{Call: _e.mock.On("GetPropertyFrequency")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyFrequency_Call) Run(run func()) *MockAccessPoint_GetPropertyFrequency_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyFrequency_Call) Return(_a0 uint32, _a1 error) *MockAccessPoint_GetPropertyFrequency_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyFrequency_Call) RunAndReturn(run func() (uint32, error)) *MockAccessPoint_GetPropertyFrequency_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyHWAddress provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyHWAddress() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyHWAddress")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyHWAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyHWAddress'
|
||||
type MockAccessPoint_GetPropertyHWAddress_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyHWAddress is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyHWAddress() *MockAccessPoint_GetPropertyHWAddress_Call {
|
||||
return &MockAccessPoint_GetPropertyHWAddress_Call{Call: _e.mock.On("GetPropertyHWAddress")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyHWAddress_Call) Run(run func()) *MockAccessPoint_GetPropertyHWAddress_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyHWAddress_Call) Return(_a0 string, _a1 error) *MockAccessPoint_GetPropertyHWAddress_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyHWAddress_Call) RunAndReturn(run func() (string, error)) *MockAccessPoint_GetPropertyHWAddress_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyLastSeen provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyLastSeen() (int32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyLastSeen")
|
||||
}
|
||||
|
||||
var r0 int32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (int32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() int32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyLastSeen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyLastSeen'
|
||||
type MockAccessPoint_GetPropertyLastSeen_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyLastSeen is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyLastSeen() *MockAccessPoint_GetPropertyLastSeen_Call {
|
||||
return &MockAccessPoint_GetPropertyLastSeen_Call{Call: _e.mock.On("GetPropertyLastSeen")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyLastSeen_Call) Run(run func()) *MockAccessPoint_GetPropertyLastSeen_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyLastSeen_Call) Return(_a0 int32, _a1 error) *MockAccessPoint_GetPropertyLastSeen_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyLastSeen_Call) RunAndReturn(run func() (int32, error)) *MockAccessPoint_GetPropertyLastSeen_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyMaxBitrate provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyMaxBitrate() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyMaxBitrate")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyMaxBitrate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyMaxBitrate'
|
||||
type MockAccessPoint_GetPropertyMaxBitrate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyMaxBitrate is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyMaxBitrate() *MockAccessPoint_GetPropertyMaxBitrate_Call {
|
||||
return &MockAccessPoint_GetPropertyMaxBitrate_Call{Call: _e.mock.On("GetPropertyMaxBitrate")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyMaxBitrate_Call) Run(run func()) *MockAccessPoint_GetPropertyMaxBitrate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyMaxBitrate_Call) Return(_a0 uint32, _a1 error) *MockAccessPoint_GetPropertyMaxBitrate_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyMaxBitrate_Call) RunAndReturn(run func() (uint32, error)) *MockAccessPoint_GetPropertyMaxBitrate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyMode provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyMode() (gonetworkmanager.Nm80211Mode, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyMode")
|
||||
}
|
||||
|
||||
var r0 gonetworkmanager.Nm80211Mode
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (gonetworkmanager.Nm80211Mode, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() gonetworkmanager.Nm80211Mode); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(gonetworkmanager.Nm80211Mode)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyMode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyMode'
|
||||
type MockAccessPoint_GetPropertyMode_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyMode is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyMode() *MockAccessPoint_GetPropertyMode_Call {
|
||||
return &MockAccessPoint_GetPropertyMode_Call{Call: _e.mock.On("GetPropertyMode")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyMode_Call) Run(run func()) *MockAccessPoint_GetPropertyMode_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyMode_Call) Return(_a0 gonetworkmanager.Nm80211Mode, _a1 error) *MockAccessPoint_GetPropertyMode_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyMode_Call) RunAndReturn(run func() (gonetworkmanager.Nm80211Mode, error)) *MockAccessPoint_GetPropertyMode_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyRSNFlags provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyRSNFlags() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyRSNFlags")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyRSNFlags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyRSNFlags'
|
||||
type MockAccessPoint_GetPropertyRSNFlags_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyRSNFlags is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyRSNFlags() *MockAccessPoint_GetPropertyRSNFlags_Call {
|
||||
return &MockAccessPoint_GetPropertyRSNFlags_Call{Call: _e.mock.On("GetPropertyRSNFlags")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyRSNFlags_Call) Run(run func()) *MockAccessPoint_GetPropertyRSNFlags_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyRSNFlags_Call) Return(_a0 uint32, _a1 error) *MockAccessPoint_GetPropertyRSNFlags_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyRSNFlags_Call) RunAndReturn(run func() (uint32, error)) *MockAccessPoint_GetPropertyRSNFlags_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertySSID provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertySSID() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertySSID")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertySSID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertySSID'
|
||||
type MockAccessPoint_GetPropertySSID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertySSID is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertySSID() *MockAccessPoint_GetPropertySSID_Call {
|
||||
return &MockAccessPoint_GetPropertySSID_Call{Call: _e.mock.On("GetPropertySSID")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertySSID_Call) Run(run func()) *MockAccessPoint_GetPropertySSID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertySSID_Call) Return(_a0 string, _a1 error) *MockAccessPoint_GetPropertySSID_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertySSID_Call) RunAndReturn(run func() (string, error)) *MockAccessPoint_GetPropertySSID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyStrength provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyStrength() (uint8, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyStrength")
|
||||
}
|
||||
|
||||
var r0 uint8
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint8, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint8); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint8)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyStrength_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyStrength'
|
||||
type MockAccessPoint_GetPropertyStrength_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyStrength is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyStrength() *MockAccessPoint_GetPropertyStrength_Call {
|
||||
return &MockAccessPoint_GetPropertyStrength_Call{Call: _e.mock.On("GetPropertyStrength")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyStrength_Call) Run(run func()) *MockAccessPoint_GetPropertyStrength_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyStrength_Call) Return(_a0 uint8, _a1 error) *MockAccessPoint_GetPropertyStrength_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyStrength_Call) RunAndReturn(run func() (uint8, error)) *MockAccessPoint_GetPropertyStrength_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyWPAFlags provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) GetPropertyWPAFlags() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyWPAFlags")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_GetPropertyWPAFlags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyWPAFlags'
|
||||
type MockAccessPoint_GetPropertyWPAFlags_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyWPAFlags is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) GetPropertyWPAFlags() *MockAccessPoint_GetPropertyWPAFlags_Call {
|
||||
return &MockAccessPoint_GetPropertyWPAFlags_Call{Call: _e.mock.On("GetPropertyWPAFlags")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyWPAFlags_Call) Run(run func()) *MockAccessPoint_GetPropertyWPAFlags_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyWPAFlags_Call) Return(_a0 uint32, _a1 error) *MockAccessPoint_GetPropertyWPAFlags_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_GetPropertyWPAFlags_Call) RunAndReturn(run func() (uint32, error)) *MockAccessPoint_GetPropertyWPAFlags_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// MarshalJSON provides a mock function with no fields
|
||||
func (_m *MockAccessPoint) MarshalJSON() ([]byte, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MarshalJSON")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]byte, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []byte); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockAccessPoint_MarshalJSON_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarshalJSON'
|
||||
type MockAccessPoint_MarshalJSON_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// MarshalJSON is a helper method to define mock.On call
|
||||
func (_e *MockAccessPoint_Expecter) MarshalJSON() *MockAccessPoint_MarshalJSON_Call {
|
||||
return &MockAccessPoint_MarshalJSON_Call{Call: _e.mock.On("MarshalJSON")}
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_MarshalJSON_Call) Run(run func()) *MockAccessPoint_MarshalJSON_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_MarshalJSON_Call) Return(_a0 []byte, _a1 error) *MockAccessPoint_MarshalJSON_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockAccessPoint_MarshalJSON_Call) RunAndReturn(run func() ([]byte, error)) *MockAccessPoint_MarshalJSON_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockAccessPoint creates a new instance of MockAccessPoint. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockAccessPoint(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockAccessPoint {
|
||||
mock := &MockAccessPoint{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,646 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package gonetworkmanager
|
||||
|
||||
import (
|
||||
gonetworkmanager "github.com/Wifx/gonetworkmanager/v2"
|
||||
dbus "github.com/godbus/dbus/v5"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockConnection is an autogenerated mock type for the Connection type
|
||||
type MockConnection struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockConnection_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockConnection) EXPECT() *MockConnection_Expecter {
|
||||
return &MockConnection_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// ClearSecrets provides a mock function with no fields
|
||||
func (_m *MockConnection) ClearSecrets() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ClearSecrets")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_ClearSecrets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClearSecrets'
|
||||
type MockConnection_ClearSecrets_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ClearSecrets is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) ClearSecrets() *MockConnection_ClearSecrets_Call {
|
||||
return &MockConnection_ClearSecrets_Call{Call: _e.mock.On("ClearSecrets")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_ClearSecrets_Call) Run(run func()) *MockConnection_ClearSecrets_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_ClearSecrets_Call) Return(_a0 error) *MockConnection_ClearSecrets_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_ClearSecrets_Call) RunAndReturn(run func() error) *MockConnection_ClearSecrets_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Delete provides a mock function with no fields
|
||||
func (_m *MockConnection) Delete() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Delete")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
|
||||
type MockConnection_Delete_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Delete is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) Delete() *MockConnection_Delete_Call {
|
||||
return &MockConnection_Delete_Call{Call: _e.mock.On("Delete")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Delete_Call) Run(run func()) *MockConnection_Delete_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Delete_Call) Return(_a0 error) *MockConnection_Delete_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Delete_Call) RunAndReturn(run func() error) *MockConnection_Delete_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPath provides a mock function with no fields
|
||||
func (_m *MockConnection) GetPath() dbus.ObjectPath {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPath")
|
||||
}
|
||||
|
||||
var r0 dbus.ObjectPath
|
||||
if rf, ok := ret.Get(0).(func() dbus.ObjectPath); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_GetPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPath'
|
||||
type MockConnection_GetPath_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPath is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) GetPath() *MockConnection_GetPath_Call {
|
||||
return &MockConnection_GetPath_Call{Call: _e.mock.On("GetPath")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPath_Call) Run(run func()) *MockConnection_GetPath_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPath_Call) Return(_a0 dbus.ObjectPath) *MockConnection_GetPath_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPath_Call) RunAndReturn(run func() dbus.ObjectPath) *MockConnection_GetPath_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyFilename provides a mock function with no fields
|
||||
func (_m *MockConnection) GetPropertyFilename() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyFilename")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConnection_GetPropertyFilename_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyFilename'
|
||||
type MockConnection_GetPropertyFilename_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyFilename is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) GetPropertyFilename() *MockConnection_GetPropertyFilename_Call {
|
||||
return &MockConnection_GetPropertyFilename_Call{Call: _e.mock.On("GetPropertyFilename")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyFilename_Call) Run(run func()) *MockConnection_GetPropertyFilename_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyFilename_Call) Return(_a0 string, _a1 error) *MockConnection_GetPropertyFilename_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyFilename_Call) RunAndReturn(run func() (string, error)) *MockConnection_GetPropertyFilename_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyFlags provides a mock function with no fields
|
||||
func (_m *MockConnection) GetPropertyFlags() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyFlags")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConnection_GetPropertyFlags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyFlags'
|
||||
type MockConnection_GetPropertyFlags_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyFlags is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) GetPropertyFlags() *MockConnection_GetPropertyFlags_Call {
|
||||
return &MockConnection_GetPropertyFlags_Call{Call: _e.mock.On("GetPropertyFlags")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyFlags_Call) Run(run func()) *MockConnection_GetPropertyFlags_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyFlags_Call) Return(_a0 uint32, _a1 error) *MockConnection_GetPropertyFlags_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyFlags_Call) RunAndReturn(run func() (uint32, error)) *MockConnection_GetPropertyFlags_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyUnsaved provides a mock function with no fields
|
||||
func (_m *MockConnection) GetPropertyUnsaved() (bool, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyUnsaved")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (bool, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConnection_GetPropertyUnsaved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyUnsaved'
|
||||
type MockConnection_GetPropertyUnsaved_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyUnsaved is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) GetPropertyUnsaved() *MockConnection_GetPropertyUnsaved_Call {
|
||||
return &MockConnection_GetPropertyUnsaved_Call{Call: _e.mock.On("GetPropertyUnsaved")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyUnsaved_Call) Run(run func()) *MockConnection_GetPropertyUnsaved_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyUnsaved_Call) Return(_a0 bool, _a1 error) *MockConnection_GetPropertyUnsaved_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetPropertyUnsaved_Call) RunAndReturn(run func() (bool, error)) *MockConnection_GetPropertyUnsaved_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetSecrets provides a mock function with given fields: settingName
|
||||
func (_m *MockConnection) GetSecrets(settingName string) (gonetworkmanager.ConnectionSettings, error) {
|
||||
ret := _m.Called(settingName)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetSecrets")
|
||||
}
|
||||
|
||||
var r0 gonetworkmanager.ConnectionSettings
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (gonetworkmanager.ConnectionSettings, error)); ok {
|
||||
return rf(settingName)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) gonetworkmanager.ConnectionSettings); ok {
|
||||
r0 = rf(settingName)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(gonetworkmanager.ConnectionSettings)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(settingName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConnection_GetSecrets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSecrets'
|
||||
type MockConnection_GetSecrets_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetSecrets is a helper method to define mock.On call
|
||||
// - settingName string
|
||||
func (_e *MockConnection_Expecter) GetSecrets(settingName interface{}) *MockConnection_GetSecrets_Call {
|
||||
return &MockConnection_GetSecrets_Call{Call: _e.mock.On("GetSecrets", settingName)}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetSecrets_Call) Run(run func(settingName string)) *MockConnection_GetSecrets_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetSecrets_Call) Return(_a0 gonetworkmanager.ConnectionSettings, _a1 error) *MockConnection_GetSecrets_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetSecrets_Call) RunAndReturn(run func(string) (gonetworkmanager.ConnectionSettings, error)) *MockConnection_GetSecrets_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetSettings provides a mock function with no fields
|
||||
func (_m *MockConnection) GetSettings() (gonetworkmanager.ConnectionSettings, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetSettings")
|
||||
}
|
||||
|
||||
var r0 gonetworkmanager.ConnectionSettings
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (gonetworkmanager.ConnectionSettings, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() gonetworkmanager.ConnectionSettings); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(gonetworkmanager.ConnectionSettings)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConnection_GetSettings_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSettings'
|
||||
type MockConnection_GetSettings_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetSettings is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) GetSettings() *MockConnection_GetSettings_Call {
|
||||
return &MockConnection_GetSettings_Call{Call: _e.mock.On("GetSettings")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetSettings_Call) Run(run func()) *MockConnection_GetSettings_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetSettings_Call) Return(_a0 gonetworkmanager.ConnectionSettings, _a1 error) *MockConnection_GetSettings_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_GetSettings_Call) RunAndReturn(run func() (gonetworkmanager.ConnectionSettings, error)) *MockConnection_GetSettings_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// MarshalJSON provides a mock function with no fields
|
||||
func (_m *MockConnection) MarshalJSON() ([]byte, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MarshalJSON")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]byte, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []byte); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConnection_MarshalJSON_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarshalJSON'
|
||||
type MockConnection_MarshalJSON_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// MarshalJSON is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) MarshalJSON() *MockConnection_MarshalJSON_Call {
|
||||
return &MockConnection_MarshalJSON_Call{Call: _e.mock.On("MarshalJSON")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_MarshalJSON_Call) Run(run func()) *MockConnection_MarshalJSON_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_MarshalJSON_Call) Return(_a0 []byte, _a1 error) *MockConnection_MarshalJSON_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_MarshalJSON_Call) RunAndReturn(run func() ([]byte, error)) *MockConnection_MarshalJSON_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Save provides a mock function with no fields
|
||||
func (_m *MockConnection) Save() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
|
||||
type MockConnection_Save_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Save is a helper method to define mock.On call
|
||||
func (_e *MockConnection_Expecter) Save() *MockConnection_Save_Call {
|
||||
return &MockConnection_Save_Call{Call: _e.mock.On("Save")}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Save_Call) Run(run func()) *MockConnection_Save_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Save_Call) Return(_a0 error) *MockConnection_Save_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Save_Call) RunAndReturn(run func() error) *MockConnection_Save_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: settings
|
||||
func (_m *MockConnection) Update(settings gonetworkmanager.ConnectionSettings) error {
|
||||
ret := _m.Called(settings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(gonetworkmanager.ConnectionSettings) error); ok {
|
||||
r0 = rf(settings)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
|
||||
type MockConnection_Update_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Update is a helper method to define mock.On call
|
||||
// - settings gonetworkmanager.ConnectionSettings
|
||||
func (_e *MockConnection_Expecter) Update(settings interface{}) *MockConnection_Update_Call {
|
||||
return &MockConnection_Update_Call{Call: _e.mock.On("Update", settings)}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Update_Call) Run(run func(settings gonetworkmanager.ConnectionSettings)) *MockConnection_Update_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(gonetworkmanager.ConnectionSettings))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Update_Call) Return(_a0 error) *MockConnection_Update_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_Update_Call) RunAndReturn(run func(gonetworkmanager.ConnectionSettings) error) *MockConnection_Update_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateUnsaved provides a mock function with given fields: settings
|
||||
func (_m *MockConnection) UpdateUnsaved(settings gonetworkmanager.ConnectionSettings) error {
|
||||
ret := _m.Called(settings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateUnsaved")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(gonetworkmanager.ConnectionSettings) error); ok {
|
||||
r0 = rf(settings)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConnection_UpdateUnsaved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUnsaved'
|
||||
type MockConnection_UpdateUnsaved_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateUnsaved is a helper method to define mock.On call
|
||||
// - settings gonetworkmanager.ConnectionSettings
|
||||
func (_e *MockConnection_Expecter) UpdateUnsaved(settings interface{}) *MockConnection_UpdateUnsaved_Call {
|
||||
return &MockConnection_UpdateUnsaved_Call{Call: _e.mock.On("UpdateUnsaved", settings)}
|
||||
}
|
||||
|
||||
func (_c *MockConnection_UpdateUnsaved_Call) Run(run func(settings gonetworkmanager.ConnectionSettings)) *MockConnection_UpdateUnsaved_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(gonetworkmanager.ConnectionSettings))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_UpdateUnsaved_Call) Return(_a0 error) *MockConnection_UpdateUnsaved_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConnection_UpdateUnsaved_Call) RunAndReturn(run func(gonetworkmanager.ConnectionSettings) error) *MockConnection_UpdateUnsaved_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockConnection creates a new instance of MockConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockConnection(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockConnection {
|
||||
mock := &MockConnection{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
1638
internal/mocks/github.com/Wifx/gonetworkmanager/v2/mock_Device.go
Normal file
1638
internal/mocks/github.com/Wifx/gonetworkmanager/v2/mock_Device.go
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,772 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package gonetworkmanager
|
||||
|
||||
import (
|
||||
gonetworkmanager "github.com/Wifx/gonetworkmanager/v2"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockIP4Config is an autogenerated mock type for the IP4Config type
|
||||
type MockIP4Config struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockIP4Config_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockIP4Config) EXPECT() *MockIP4Config_Expecter {
|
||||
return &MockIP4Config_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetPropertyAddressData provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyAddressData() ([]gonetworkmanager.IP4AddressData, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyAddressData")
|
||||
}
|
||||
|
||||
var r0 []gonetworkmanager.IP4AddressData
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]gonetworkmanager.IP4AddressData, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []gonetworkmanager.IP4AddressData); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]gonetworkmanager.IP4AddressData)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyAddressData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyAddressData'
|
||||
type MockIP4Config_GetPropertyAddressData_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyAddressData is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyAddressData() *MockIP4Config_GetPropertyAddressData_Call {
|
||||
return &MockIP4Config_GetPropertyAddressData_Call{Call: _e.mock.On("GetPropertyAddressData")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyAddressData_Call) Run(run func()) *MockIP4Config_GetPropertyAddressData_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyAddressData_Call) Return(_a0 []gonetworkmanager.IP4AddressData, _a1 error) *MockIP4Config_GetPropertyAddressData_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyAddressData_Call) RunAndReturn(run func() ([]gonetworkmanager.IP4AddressData, error)) *MockIP4Config_GetPropertyAddressData_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyAddresses provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyAddresses() ([]gonetworkmanager.IP4Address, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyAddresses")
|
||||
}
|
||||
|
||||
var r0 []gonetworkmanager.IP4Address
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]gonetworkmanager.IP4Address, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []gonetworkmanager.IP4Address); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]gonetworkmanager.IP4Address)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyAddresses_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyAddresses'
|
||||
type MockIP4Config_GetPropertyAddresses_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyAddresses is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyAddresses() *MockIP4Config_GetPropertyAddresses_Call {
|
||||
return &MockIP4Config_GetPropertyAddresses_Call{Call: _e.mock.On("GetPropertyAddresses")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyAddresses_Call) Run(run func()) *MockIP4Config_GetPropertyAddresses_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyAddresses_Call) Return(_a0 []gonetworkmanager.IP4Address, _a1 error) *MockIP4Config_GetPropertyAddresses_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyAddresses_Call) RunAndReturn(run func() ([]gonetworkmanager.IP4Address, error)) *MockIP4Config_GetPropertyAddresses_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyDnsOptions provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyDnsOptions() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyDnsOptions")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyDnsOptions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyDnsOptions'
|
||||
type MockIP4Config_GetPropertyDnsOptions_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyDnsOptions is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyDnsOptions() *MockIP4Config_GetPropertyDnsOptions_Call {
|
||||
return &MockIP4Config_GetPropertyDnsOptions_Call{Call: _e.mock.On("GetPropertyDnsOptions")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDnsOptions_Call) Run(run func()) *MockIP4Config_GetPropertyDnsOptions_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDnsOptions_Call) Return(_a0 []string, _a1 error) *MockIP4Config_GetPropertyDnsOptions_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDnsOptions_Call) RunAndReturn(run func() ([]string, error)) *MockIP4Config_GetPropertyDnsOptions_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyDnsPriority provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyDnsPriority() (uint32, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyDnsPriority")
|
||||
}
|
||||
|
||||
var r0 uint32
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (uint32, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() uint32); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint32)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyDnsPriority_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyDnsPriority'
|
||||
type MockIP4Config_GetPropertyDnsPriority_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyDnsPriority is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyDnsPriority() *MockIP4Config_GetPropertyDnsPriority_Call {
|
||||
return &MockIP4Config_GetPropertyDnsPriority_Call{Call: _e.mock.On("GetPropertyDnsPriority")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDnsPriority_Call) Run(run func()) *MockIP4Config_GetPropertyDnsPriority_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDnsPriority_Call) Return(_a0 uint32, _a1 error) *MockIP4Config_GetPropertyDnsPriority_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDnsPriority_Call) RunAndReturn(run func() (uint32, error)) *MockIP4Config_GetPropertyDnsPriority_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyDomains provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyDomains() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyDomains")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyDomains_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyDomains'
|
||||
type MockIP4Config_GetPropertyDomains_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyDomains is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyDomains() *MockIP4Config_GetPropertyDomains_Call {
|
||||
return &MockIP4Config_GetPropertyDomains_Call{Call: _e.mock.On("GetPropertyDomains")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDomains_Call) Run(run func()) *MockIP4Config_GetPropertyDomains_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDomains_Call) Return(_a0 []string, _a1 error) *MockIP4Config_GetPropertyDomains_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyDomains_Call) RunAndReturn(run func() ([]string, error)) *MockIP4Config_GetPropertyDomains_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyGateway provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyGateway() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyGateway")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyGateway_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyGateway'
|
||||
type MockIP4Config_GetPropertyGateway_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyGateway is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyGateway() *MockIP4Config_GetPropertyGateway_Call {
|
||||
return &MockIP4Config_GetPropertyGateway_Call{Call: _e.mock.On("GetPropertyGateway")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyGateway_Call) Run(run func()) *MockIP4Config_GetPropertyGateway_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyGateway_Call) Return(_a0 string, _a1 error) *MockIP4Config_GetPropertyGateway_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyGateway_Call) RunAndReturn(run func() (string, error)) *MockIP4Config_GetPropertyGateway_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyNameserverData provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyNameserverData() ([]gonetworkmanager.IP4NameserverData, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyNameserverData")
|
||||
}
|
||||
|
||||
var r0 []gonetworkmanager.IP4NameserverData
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]gonetworkmanager.IP4NameserverData, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []gonetworkmanager.IP4NameserverData); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]gonetworkmanager.IP4NameserverData)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyNameserverData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyNameserverData'
|
||||
type MockIP4Config_GetPropertyNameserverData_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyNameserverData is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyNameserverData() *MockIP4Config_GetPropertyNameserverData_Call {
|
||||
return &MockIP4Config_GetPropertyNameserverData_Call{Call: _e.mock.On("GetPropertyNameserverData")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyNameserverData_Call) Run(run func()) *MockIP4Config_GetPropertyNameserverData_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyNameserverData_Call) Return(_a0 []gonetworkmanager.IP4NameserverData, _a1 error) *MockIP4Config_GetPropertyNameserverData_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyNameserverData_Call) RunAndReturn(run func() ([]gonetworkmanager.IP4NameserverData, error)) *MockIP4Config_GetPropertyNameserverData_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyNameservers provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyNameservers() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyNameservers")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyNameservers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyNameservers'
|
||||
type MockIP4Config_GetPropertyNameservers_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyNameservers is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyNameservers() *MockIP4Config_GetPropertyNameservers_Call {
|
||||
return &MockIP4Config_GetPropertyNameservers_Call{Call: _e.mock.On("GetPropertyNameservers")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyNameservers_Call) Run(run func()) *MockIP4Config_GetPropertyNameservers_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyNameservers_Call) Return(_a0 []string, _a1 error) *MockIP4Config_GetPropertyNameservers_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyNameservers_Call) RunAndReturn(run func() ([]string, error)) *MockIP4Config_GetPropertyNameservers_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyRouteData provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyRouteData() ([]gonetworkmanager.IP4RouteData, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyRouteData")
|
||||
}
|
||||
|
||||
var r0 []gonetworkmanager.IP4RouteData
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]gonetworkmanager.IP4RouteData, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []gonetworkmanager.IP4RouteData); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]gonetworkmanager.IP4RouteData)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyRouteData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyRouteData'
|
||||
type MockIP4Config_GetPropertyRouteData_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyRouteData is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyRouteData() *MockIP4Config_GetPropertyRouteData_Call {
|
||||
return &MockIP4Config_GetPropertyRouteData_Call{Call: _e.mock.On("GetPropertyRouteData")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyRouteData_Call) Run(run func()) *MockIP4Config_GetPropertyRouteData_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyRouteData_Call) Return(_a0 []gonetworkmanager.IP4RouteData, _a1 error) *MockIP4Config_GetPropertyRouteData_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyRouteData_Call) RunAndReturn(run func() ([]gonetworkmanager.IP4RouteData, error)) *MockIP4Config_GetPropertyRouteData_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyRoutes provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyRoutes() ([]gonetworkmanager.IP4Route, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyRoutes")
|
||||
}
|
||||
|
||||
var r0 []gonetworkmanager.IP4Route
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]gonetworkmanager.IP4Route, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []gonetworkmanager.IP4Route); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]gonetworkmanager.IP4Route)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyRoutes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyRoutes'
|
||||
type MockIP4Config_GetPropertyRoutes_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyRoutes is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyRoutes() *MockIP4Config_GetPropertyRoutes_Call {
|
||||
return &MockIP4Config_GetPropertyRoutes_Call{Call: _e.mock.On("GetPropertyRoutes")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyRoutes_Call) Run(run func()) *MockIP4Config_GetPropertyRoutes_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyRoutes_Call) Return(_a0 []gonetworkmanager.IP4Route, _a1 error) *MockIP4Config_GetPropertyRoutes_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyRoutes_Call) RunAndReturn(run func() ([]gonetworkmanager.IP4Route, error)) *MockIP4Config_GetPropertyRoutes_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertySearches provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertySearches() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertySearches")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertySearches_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertySearches'
|
||||
type MockIP4Config_GetPropertySearches_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertySearches is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertySearches() *MockIP4Config_GetPropertySearches_Call {
|
||||
return &MockIP4Config_GetPropertySearches_Call{Call: _e.mock.On("GetPropertySearches")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertySearches_Call) Run(run func()) *MockIP4Config_GetPropertySearches_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertySearches_Call) Return(_a0 []string, _a1 error) *MockIP4Config_GetPropertySearches_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertySearches_Call) RunAndReturn(run func() ([]string, error)) *MockIP4Config_GetPropertySearches_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyWinsServerData provides a mock function with no fields
|
||||
func (_m *MockIP4Config) GetPropertyWinsServerData() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyWinsServerData")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_GetPropertyWinsServerData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyWinsServerData'
|
||||
type MockIP4Config_GetPropertyWinsServerData_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyWinsServerData is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) GetPropertyWinsServerData() *MockIP4Config_GetPropertyWinsServerData_Call {
|
||||
return &MockIP4Config_GetPropertyWinsServerData_Call{Call: _e.mock.On("GetPropertyWinsServerData")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyWinsServerData_Call) Run(run func()) *MockIP4Config_GetPropertyWinsServerData_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyWinsServerData_Call) Return(_a0 []string, _a1 error) *MockIP4Config_GetPropertyWinsServerData_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_GetPropertyWinsServerData_Call) RunAndReturn(run func() ([]string, error)) *MockIP4Config_GetPropertyWinsServerData_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// MarshalJSON provides a mock function with no fields
|
||||
func (_m *MockIP4Config) MarshalJSON() ([]byte, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for MarshalJSON")
|
||||
}
|
||||
|
||||
var r0 []byte
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]byte, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []byte); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockIP4Config_MarshalJSON_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarshalJSON'
|
||||
type MockIP4Config_MarshalJSON_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// MarshalJSON is a helper method to define mock.On call
|
||||
func (_e *MockIP4Config_Expecter) MarshalJSON() *MockIP4Config_MarshalJSON_Call {
|
||||
return &MockIP4Config_MarshalJSON_Call{Call: _e.mock.On("MarshalJSON")}
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_MarshalJSON_Call) Run(run func()) *MockIP4Config_MarshalJSON_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_MarshalJSON_Call) Return(_a0 []byte, _a1 error) *MockIP4Config_MarshalJSON_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockIP4Config_MarshalJSON_Call) RunAndReturn(run func() ([]byte, error)) *MockIP4Config_MarshalJSON_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockIP4Config creates a new instance of MockIP4Config. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockIP4Config(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockIP4Config {
|
||||
mock := &MockIP4Config{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,467 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package gonetworkmanager
|
||||
|
||||
import (
|
||||
gonetworkmanager "github.com/Wifx/gonetworkmanager/v2"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockSettings is an autogenerated mock type for the Settings type
|
||||
type MockSettings struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockSettings_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockSettings) EXPECT() *MockSettings_Expecter {
|
||||
return &MockSettings_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// AddConnection provides a mock function with given fields: settings
|
||||
func (_m *MockSettings) AddConnection(settings gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error) {
|
||||
ret := _m.Called(settings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddConnection")
|
||||
}
|
||||
|
||||
var r0 gonetworkmanager.Connection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error)); ok {
|
||||
return rf(settings)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(gonetworkmanager.ConnectionSettings) gonetworkmanager.Connection); ok {
|
||||
r0 = rf(settings)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(gonetworkmanager.Connection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(gonetworkmanager.ConnectionSettings) error); ok {
|
||||
r1 = rf(settings)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSettings_AddConnection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddConnection'
|
||||
type MockSettings_AddConnection_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddConnection is a helper method to define mock.On call
|
||||
// - settings gonetworkmanager.ConnectionSettings
|
||||
func (_e *MockSettings_Expecter) AddConnection(settings interface{}) *MockSettings_AddConnection_Call {
|
||||
return &MockSettings_AddConnection_Call{Call: _e.mock.On("AddConnection", settings)}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_AddConnection_Call) Run(run func(settings gonetworkmanager.ConnectionSettings)) *MockSettings_AddConnection_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(gonetworkmanager.ConnectionSettings))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_AddConnection_Call) Return(_a0 gonetworkmanager.Connection, _a1 error) *MockSettings_AddConnection_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_AddConnection_Call) RunAndReturn(run func(gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error)) *MockSettings_AddConnection_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddConnectionUnsaved provides a mock function with given fields: settings
|
||||
func (_m *MockSettings) AddConnectionUnsaved(settings gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error) {
|
||||
ret := _m.Called(settings)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddConnectionUnsaved")
|
||||
}
|
||||
|
||||
var r0 gonetworkmanager.Connection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error)); ok {
|
||||
return rf(settings)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(gonetworkmanager.ConnectionSettings) gonetworkmanager.Connection); ok {
|
||||
r0 = rf(settings)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(gonetworkmanager.Connection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(gonetworkmanager.ConnectionSettings) error); ok {
|
||||
r1 = rf(settings)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSettings_AddConnectionUnsaved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddConnectionUnsaved'
|
||||
type MockSettings_AddConnectionUnsaved_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddConnectionUnsaved is a helper method to define mock.On call
|
||||
// - settings gonetworkmanager.ConnectionSettings
|
||||
func (_e *MockSettings_Expecter) AddConnectionUnsaved(settings interface{}) *MockSettings_AddConnectionUnsaved_Call {
|
||||
return &MockSettings_AddConnectionUnsaved_Call{Call: _e.mock.On("AddConnectionUnsaved", settings)}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_AddConnectionUnsaved_Call) Run(run func(settings gonetworkmanager.ConnectionSettings)) *MockSettings_AddConnectionUnsaved_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(gonetworkmanager.ConnectionSettings))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_AddConnectionUnsaved_Call) Return(_a0 gonetworkmanager.Connection, _a1 error) *MockSettings_AddConnectionUnsaved_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_AddConnectionUnsaved_Call) RunAndReturn(run func(gonetworkmanager.ConnectionSettings) (gonetworkmanager.Connection, error)) *MockSettings_AddConnectionUnsaved_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetConnectionByUUID provides a mock function with given fields: uuid
|
||||
func (_m *MockSettings) GetConnectionByUUID(uuid string) (gonetworkmanager.Connection, error) {
|
||||
ret := _m.Called(uuid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetConnectionByUUID")
|
||||
}
|
||||
|
||||
var r0 gonetworkmanager.Connection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (gonetworkmanager.Connection, error)); ok {
|
||||
return rf(uuid)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) gonetworkmanager.Connection); ok {
|
||||
r0 = rf(uuid)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(gonetworkmanager.Connection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(uuid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSettings_GetConnectionByUUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConnectionByUUID'
|
||||
type MockSettings_GetConnectionByUUID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetConnectionByUUID is a helper method to define mock.On call
|
||||
// - uuid string
|
||||
func (_e *MockSettings_Expecter) GetConnectionByUUID(uuid interface{}) *MockSettings_GetConnectionByUUID_Call {
|
||||
return &MockSettings_GetConnectionByUUID_Call{Call: _e.mock.On("GetConnectionByUUID", uuid)}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetConnectionByUUID_Call) Run(run func(uuid string)) *MockSettings_GetConnectionByUUID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetConnectionByUUID_Call) Return(_a0 gonetworkmanager.Connection, _a1 error) *MockSettings_GetConnectionByUUID_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetConnectionByUUID_Call) RunAndReturn(run func(string) (gonetworkmanager.Connection, error)) *MockSettings_GetConnectionByUUID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyCanModify provides a mock function with no fields
|
||||
func (_m *MockSettings) GetPropertyCanModify() (bool, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyCanModify")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (bool, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSettings_GetPropertyCanModify_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyCanModify'
|
||||
type MockSettings_GetPropertyCanModify_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyCanModify is a helper method to define mock.On call
|
||||
func (_e *MockSettings_Expecter) GetPropertyCanModify() *MockSettings_GetPropertyCanModify_Call {
|
||||
return &MockSettings_GetPropertyCanModify_Call{Call: _e.mock.On("GetPropertyCanModify")}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetPropertyCanModify_Call) Run(run func()) *MockSettings_GetPropertyCanModify_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetPropertyCanModify_Call) Return(_a0 bool, _a1 error) *MockSettings_GetPropertyCanModify_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetPropertyCanModify_Call) RunAndReturn(run func() (bool, error)) *MockSettings_GetPropertyCanModify_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetPropertyHostname provides a mock function with no fields
|
||||
func (_m *MockSettings) GetPropertyHostname() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetPropertyHostname")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSettings_GetPropertyHostname_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPropertyHostname'
|
||||
type MockSettings_GetPropertyHostname_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetPropertyHostname is a helper method to define mock.On call
|
||||
func (_e *MockSettings_Expecter) GetPropertyHostname() *MockSettings_GetPropertyHostname_Call {
|
||||
return &MockSettings_GetPropertyHostname_Call{Call: _e.mock.On("GetPropertyHostname")}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetPropertyHostname_Call) Run(run func()) *MockSettings_GetPropertyHostname_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetPropertyHostname_Call) Return(_a0 string, _a1 error) *MockSettings_GetPropertyHostname_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_GetPropertyHostname_Call) RunAndReturn(run func() (string, error)) *MockSettings_GetPropertyHostname_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListConnections provides a mock function with no fields
|
||||
func (_m *MockSettings) ListConnections() ([]gonetworkmanager.Connection, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListConnections")
|
||||
}
|
||||
|
||||
var r0 []gonetworkmanager.Connection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() ([]gonetworkmanager.Connection, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() []gonetworkmanager.Connection); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]gonetworkmanager.Connection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockSettings_ListConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListConnections'
|
||||
type MockSettings_ListConnections_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListConnections is a helper method to define mock.On call
|
||||
func (_e *MockSettings_Expecter) ListConnections() *MockSettings_ListConnections_Call {
|
||||
return &MockSettings_ListConnections_Call{Call: _e.mock.On("ListConnections")}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_ListConnections_Call) Run(run func()) *MockSettings_ListConnections_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_ListConnections_Call) Return(_a0 []gonetworkmanager.Connection, _a1 error) *MockSettings_ListConnections_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_ListConnections_Call) RunAndReturn(run func() ([]gonetworkmanager.Connection, error)) *MockSettings_ListConnections_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ReloadConnections provides a mock function with no fields
|
||||
func (_m *MockSettings) ReloadConnections() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ReloadConnections")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockSettings_ReloadConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReloadConnections'
|
||||
type MockSettings_ReloadConnections_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ReloadConnections is a helper method to define mock.On call
|
||||
func (_e *MockSettings_Expecter) ReloadConnections() *MockSettings_ReloadConnections_Call {
|
||||
return &MockSettings_ReloadConnections_Call{Call: _e.mock.On("ReloadConnections")}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_ReloadConnections_Call) Run(run func()) *MockSettings_ReloadConnections_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_ReloadConnections_Call) Return(_a0 error) *MockSettings_ReloadConnections_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_ReloadConnections_Call) RunAndReturn(run func() error) *MockSettings_ReloadConnections_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SaveHostname provides a mock function with given fields: hostname
|
||||
func (_m *MockSettings) SaveHostname(hostname string) error {
|
||||
ret := _m.Called(hostname)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SaveHostname")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(hostname)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockSettings_SaveHostname_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveHostname'
|
||||
type MockSettings_SaveHostname_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SaveHostname is a helper method to define mock.On call
|
||||
// - hostname string
|
||||
func (_e *MockSettings_Expecter) SaveHostname(hostname interface{}) *MockSettings_SaveHostname_Call {
|
||||
return &MockSettings_SaveHostname_Call{Call: _e.mock.On("SaveHostname", hostname)}
|
||||
}
|
||||
|
||||
func (_c *MockSettings_SaveHostname_Call) Run(run func(hostname string)) *MockSettings_SaveHostname_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_SaveHostname_Call) Return(_a0 error) *MockSettings_SaveHostname_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockSettings_SaveHostname_Call) RunAndReturn(run func(string) error) *MockSettings_SaveHostname_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockSettings creates a new instance of MockSettings. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockSettings(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockSettings {
|
||||
mock := &MockSettings{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
649
internal/mocks/github.com/godbus/dbus/v5/mock_BusObject.go
Normal file
649
internal/mocks/github.com/godbus/dbus/v5/mock_BusObject.go
Normal file
@@ -0,0 +1,649 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package dbus
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
dbus "github.com/godbus/dbus/v5"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockBusObject is an autogenerated mock type for the BusObject type
|
||||
type MockBusObject struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockBusObject_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockBusObject) EXPECT() *MockBusObject_Expecter {
|
||||
return &MockBusObject_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// AddMatchSignal provides a mock function with given fields: iface, member, options
|
||||
func (_m *MockBusObject) AddMatchSignal(iface string, member string, options ...dbus.MatchOption) *dbus.Call {
|
||||
_va := make([]interface{}, len(options))
|
||||
for _i := range options {
|
||||
_va[_i] = options[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, iface, member)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddMatchSignal")
|
||||
}
|
||||
|
||||
var r0 *dbus.Call
|
||||
if rf, ok := ret.Get(0).(func(string, string, ...dbus.MatchOption) *dbus.Call); ok {
|
||||
r0 = rf(iface, member, options...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dbus.Call)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_AddMatchSignal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddMatchSignal'
|
||||
type MockBusObject_AddMatchSignal_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddMatchSignal is a helper method to define mock.On call
|
||||
// - iface string
|
||||
// - member string
|
||||
// - options ...dbus.MatchOption
|
||||
func (_e *MockBusObject_Expecter) AddMatchSignal(iface interface{}, member interface{}, options ...interface{}) *MockBusObject_AddMatchSignal_Call {
|
||||
return &MockBusObject_AddMatchSignal_Call{Call: _e.mock.On("AddMatchSignal",
|
||||
append([]interface{}{iface, member}, options...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_AddMatchSignal_Call) Run(run func(iface string, member string, options ...dbus.MatchOption)) *MockBusObject_AddMatchSignal_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]dbus.MatchOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(dbus.MatchOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(string), args[1].(string), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_AddMatchSignal_Call) Return(_a0 *dbus.Call) *MockBusObject_AddMatchSignal_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_AddMatchSignal_Call) RunAndReturn(run func(string, string, ...dbus.MatchOption) *dbus.Call) *MockBusObject_AddMatchSignal_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Call provides a mock function with given fields: method, flags, args
|
||||
func (_m *MockBusObject) Call(method string, flags dbus.Flags, args ...interface{}) *dbus.Call {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, method, flags)
|
||||
_ca = append(_ca, args...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Call")
|
||||
}
|
||||
|
||||
var r0 *dbus.Call
|
||||
if rf, ok := ret.Get(0).(func(string, dbus.Flags, ...interface{}) *dbus.Call); ok {
|
||||
r0 = rf(method, flags, args...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dbus.Call)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_Call_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Call'
|
||||
type MockBusObject_Call_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Call is a helper method to define mock.On call
|
||||
// - method string
|
||||
// - flags dbus.Flags
|
||||
// - args ...interface{}
|
||||
func (_e *MockBusObject_Expecter) Call(method interface{}, flags interface{}, args ...interface{}) *MockBusObject_Call_Call {
|
||||
return &MockBusObject_Call_Call{Call: _e.mock.On("Call",
|
||||
append([]interface{}{method, flags}, args...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Call_Call) Run(run func(method string, flags dbus.Flags, args ...interface{})) *MockBusObject_Call_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]interface{}, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(interface{})
|
||||
}
|
||||
}
|
||||
run(args[0].(string), args[1].(dbus.Flags), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Call_Call) Return(_a0 *dbus.Call) *MockBusObject_Call_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Call_Call) RunAndReturn(run func(string, dbus.Flags, ...interface{}) *dbus.Call) *MockBusObject_Call_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// CallWithContext provides a mock function with given fields: ctx, method, flags, args
|
||||
func (_m *MockBusObject) CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, method, flags)
|
||||
_ca = append(_ca, args...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CallWithContext")
|
||||
}
|
||||
|
||||
var r0 *dbus.Call
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, dbus.Flags, ...interface{}) *dbus.Call); ok {
|
||||
r0 = rf(ctx, method, flags, args...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dbus.Call)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_CallWithContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CallWithContext'
|
||||
type MockBusObject_CallWithContext_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CallWithContext is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - method string
|
||||
// - flags dbus.Flags
|
||||
// - args ...interface{}
|
||||
func (_e *MockBusObject_Expecter) CallWithContext(ctx interface{}, method interface{}, flags interface{}, args ...interface{}) *MockBusObject_CallWithContext_Call {
|
||||
return &MockBusObject_CallWithContext_Call{Call: _e.mock.On("CallWithContext",
|
||||
append([]interface{}{ctx, method, flags}, args...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_CallWithContext_Call) Run(run func(ctx context.Context, method string, flags dbus.Flags, args ...interface{})) *MockBusObject_CallWithContext_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]interface{}, len(args)-3)
|
||||
for i, a := range args[3:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(interface{})
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(dbus.Flags), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_CallWithContext_Call) Return(_a0 *dbus.Call) *MockBusObject_CallWithContext_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_CallWithContext_Call) RunAndReturn(run func(context.Context, string, dbus.Flags, ...interface{}) *dbus.Call) *MockBusObject_CallWithContext_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Destination provides a mock function with no fields
|
||||
func (_m *MockBusObject) Destination() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Destination")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_Destination_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Destination'
|
||||
type MockBusObject_Destination_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Destination is a helper method to define mock.On call
|
||||
func (_e *MockBusObject_Expecter) Destination() *MockBusObject_Destination_Call {
|
||||
return &MockBusObject_Destination_Call{Call: _e.mock.On("Destination")}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Destination_Call) Run(run func()) *MockBusObject_Destination_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Destination_Call) Return(_a0 string) *MockBusObject_Destination_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Destination_Call) RunAndReturn(run func() string) *MockBusObject_Destination_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetProperty provides a mock function with given fields: p
|
||||
func (_m *MockBusObject) GetProperty(p string) (dbus.Variant, error) {
|
||||
ret := _m.Called(p)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetProperty")
|
||||
}
|
||||
|
||||
var r0 dbus.Variant
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (dbus.Variant, error)); ok {
|
||||
return rf(p)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) dbus.Variant); ok {
|
||||
r0 = rf(p)
|
||||
} else {
|
||||
r0 = ret.Get(0).(dbus.Variant)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(p)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockBusObject_GetProperty_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProperty'
|
||||
type MockBusObject_GetProperty_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetProperty is a helper method to define mock.On call
|
||||
// - p string
|
||||
func (_e *MockBusObject_Expecter) GetProperty(p interface{}) *MockBusObject_GetProperty_Call {
|
||||
return &MockBusObject_GetProperty_Call{Call: _e.mock.On("GetProperty", p)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_GetProperty_Call) Run(run func(p string)) *MockBusObject_GetProperty_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_GetProperty_Call) Return(_a0 dbus.Variant, _a1 error) *MockBusObject_GetProperty_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_GetProperty_Call) RunAndReturn(run func(string) (dbus.Variant, error)) *MockBusObject_GetProperty_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Go provides a mock function with given fields: method, flags, ch, args
|
||||
func (_m *MockBusObject) Go(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, method, flags, ch)
|
||||
_ca = append(_ca, args...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Go")
|
||||
}
|
||||
|
||||
var r0 *dbus.Call
|
||||
if rf, ok := ret.Get(0).(func(string, dbus.Flags, chan *dbus.Call, ...interface{}) *dbus.Call); ok {
|
||||
r0 = rf(method, flags, ch, args...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dbus.Call)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_Go_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Go'
|
||||
type MockBusObject_Go_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Go is a helper method to define mock.On call
|
||||
// - method string
|
||||
// - flags dbus.Flags
|
||||
// - ch chan *dbus.Call
|
||||
// - args ...interface{}
|
||||
func (_e *MockBusObject_Expecter) Go(method interface{}, flags interface{}, ch interface{}, args ...interface{}) *MockBusObject_Go_Call {
|
||||
return &MockBusObject_Go_Call{Call: _e.mock.On("Go",
|
||||
append([]interface{}{method, flags, ch}, args...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Go_Call) Run(run func(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{})) *MockBusObject_Go_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]interface{}, len(args)-3)
|
||||
for i, a := range args[3:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(interface{})
|
||||
}
|
||||
}
|
||||
run(args[0].(string), args[1].(dbus.Flags), args[2].(chan *dbus.Call), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Go_Call) Return(_a0 *dbus.Call) *MockBusObject_Go_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Go_Call) RunAndReturn(run func(string, dbus.Flags, chan *dbus.Call, ...interface{}) *dbus.Call) *MockBusObject_Go_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GoWithContext provides a mock function with given fields: ctx, method, flags, ch, args
|
||||
func (_m *MockBusObject) GoWithContext(ctx context.Context, method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, method, flags, ch)
|
||||
_ca = append(_ca, args...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GoWithContext")
|
||||
}
|
||||
|
||||
var r0 *dbus.Call
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, dbus.Flags, chan *dbus.Call, ...interface{}) *dbus.Call); ok {
|
||||
r0 = rf(ctx, method, flags, ch, args...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dbus.Call)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_GoWithContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GoWithContext'
|
||||
type MockBusObject_GoWithContext_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GoWithContext is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - method string
|
||||
// - flags dbus.Flags
|
||||
// - ch chan *dbus.Call
|
||||
// - args ...interface{}
|
||||
func (_e *MockBusObject_Expecter) GoWithContext(ctx interface{}, method interface{}, flags interface{}, ch interface{}, args ...interface{}) *MockBusObject_GoWithContext_Call {
|
||||
return &MockBusObject_GoWithContext_Call{Call: _e.mock.On("GoWithContext",
|
||||
append([]interface{}{ctx, method, flags, ch}, args...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_GoWithContext_Call) Run(run func(ctx context.Context, method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{})) *MockBusObject_GoWithContext_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]interface{}, len(args)-4)
|
||||
for i, a := range args[4:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(interface{})
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(string), args[2].(dbus.Flags), args[3].(chan *dbus.Call), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_GoWithContext_Call) Return(_a0 *dbus.Call) *MockBusObject_GoWithContext_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_GoWithContext_Call) RunAndReturn(run func(context.Context, string, dbus.Flags, chan *dbus.Call, ...interface{}) *dbus.Call) *MockBusObject_GoWithContext_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Path provides a mock function with no fields
|
||||
func (_m *MockBusObject) Path() dbus.ObjectPath {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Path")
|
||||
}
|
||||
|
||||
var r0 dbus.ObjectPath
|
||||
if rf, ok := ret.Get(0).(func() dbus.ObjectPath); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_Path_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Path'
|
||||
type MockBusObject_Path_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Path is a helper method to define mock.On call
|
||||
func (_e *MockBusObject_Expecter) Path() *MockBusObject_Path_Call {
|
||||
return &MockBusObject_Path_Call{Call: _e.mock.On("Path")}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Path_Call) Run(run func()) *MockBusObject_Path_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Path_Call) Return(_a0 dbus.ObjectPath) *MockBusObject_Path_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_Path_Call) RunAndReturn(run func() dbus.ObjectPath) *MockBusObject_Path_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveMatchSignal provides a mock function with given fields: iface, member, options
|
||||
func (_m *MockBusObject) RemoveMatchSignal(iface string, member string, options ...dbus.MatchOption) *dbus.Call {
|
||||
_va := make([]interface{}, len(options))
|
||||
for _i := range options {
|
||||
_va[_i] = options[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, iface, member)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveMatchSignal")
|
||||
}
|
||||
|
||||
var r0 *dbus.Call
|
||||
if rf, ok := ret.Get(0).(func(string, string, ...dbus.MatchOption) *dbus.Call); ok {
|
||||
r0 = rf(iface, member, options...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*dbus.Call)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_RemoveMatchSignal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveMatchSignal'
|
||||
type MockBusObject_RemoveMatchSignal_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveMatchSignal is a helper method to define mock.On call
|
||||
// - iface string
|
||||
// - member string
|
||||
// - options ...dbus.MatchOption
|
||||
func (_e *MockBusObject_Expecter) RemoveMatchSignal(iface interface{}, member interface{}, options ...interface{}) *MockBusObject_RemoveMatchSignal_Call {
|
||||
return &MockBusObject_RemoveMatchSignal_Call{Call: _e.mock.On("RemoveMatchSignal",
|
||||
append([]interface{}{iface, member}, options...)...)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_RemoveMatchSignal_Call) Run(run func(iface string, member string, options ...dbus.MatchOption)) *MockBusObject_RemoveMatchSignal_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]dbus.MatchOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(dbus.MatchOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(string), args[1].(string), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_RemoveMatchSignal_Call) Return(_a0 *dbus.Call) *MockBusObject_RemoveMatchSignal_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_RemoveMatchSignal_Call) RunAndReturn(run func(string, string, ...dbus.MatchOption) *dbus.Call) *MockBusObject_RemoveMatchSignal_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetProperty provides a mock function with given fields: p, v
|
||||
func (_m *MockBusObject) SetProperty(p string, v interface{}) error {
|
||||
ret := _m.Called(p, v)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetProperty")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(p, v)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_SetProperty_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetProperty'
|
||||
type MockBusObject_SetProperty_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetProperty is a helper method to define mock.On call
|
||||
// - p string
|
||||
// - v interface{}
|
||||
func (_e *MockBusObject_Expecter) SetProperty(p interface{}, v interface{}) *MockBusObject_SetProperty_Call {
|
||||
return &MockBusObject_SetProperty_Call{Call: _e.mock.On("SetProperty", p, v)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_SetProperty_Call) Run(run func(p string, v interface{})) *MockBusObject_SetProperty_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(interface{}))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_SetProperty_Call) Return(_a0 error) *MockBusObject_SetProperty_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_SetProperty_Call) RunAndReturn(run func(string, interface{}) error) *MockBusObject_SetProperty_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// StoreProperty provides a mock function with given fields: p, value
|
||||
func (_m *MockBusObject) StoreProperty(p string, value interface{}) error {
|
||||
ret := _m.Called(p, value)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for StoreProperty")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
|
||||
r0 = rf(p, value)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockBusObject_StoreProperty_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StoreProperty'
|
||||
type MockBusObject_StoreProperty_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// StoreProperty is a helper method to define mock.On call
|
||||
// - p string
|
||||
// - value interface{}
|
||||
func (_e *MockBusObject_Expecter) StoreProperty(p interface{}, value interface{}) *MockBusObject_StoreProperty_Call {
|
||||
return &MockBusObject_StoreProperty_Call{Call: _e.mock.On("StoreProperty", p, value)}
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_StoreProperty_Call) Run(run func(p string, value interface{})) *MockBusObject_StoreProperty_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(interface{}))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_StoreProperty_Call) Return(_a0 error) *MockBusObject_StoreProperty_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockBusObject_StoreProperty_Call) RunAndReturn(run func(string, interface{}) error) *MockBusObject_StoreProperty_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockBusObject creates a new instance of MockBusObject. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockBusObject(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockBusObject {
|
||||
mock := &MockBusObject{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
181
internal/mocks/internal/plugins/mock_GitClient.go
Normal file
181
internal/mocks/internal/plugins/mock_GitClient.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package plugins
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
|
||||
// MockGitClient is an autogenerated mock type for the GitClient type
|
||||
type MockGitClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockGitClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockGitClient) EXPECT() *MockGitClient_Expecter {
|
||||
return &MockGitClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// HasUpdates provides a mock function with given fields: path
|
||||
func (_m *MockGitClient) HasUpdates(path string) (bool, error) {
|
||||
ret := _m.Called(path)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for HasUpdates")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (bool, error)); ok {
|
||||
return rf(path)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(path)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(path)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockGitClient_HasUpdates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasUpdates'
|
||||
type MockGitClient_HasUpdates_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// HasUpdates is a helper method to define mock.On call
|
||||
// - path string
|
||||
func (_e *MockGitClient_Expecter) HasUpdates(path interface{}) *MockGitClient_HasUpdates_Call {
|
||||
return &MockGitClient_HasUpdates_Call{Call: _e.mock.On("HasUpdates", path)}
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_HasUpdates_Call) Run(run func(path string)) *MockGitClient_HasUpdates_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_HasUpdates_Call) Return(_a0 bool, _a1 error) *MockGitClient_HasUpdates_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_HasUpdates_Call) RunAndReturn(run func(string) (bool, error)) *MockGitClient_HasUpdates_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// PlainClone provides a mock function with given fields: path, url
|
||||
func (_m *MockGitClient) PlainClone(path string, url string) error {
|
||||
ret := _m.Called(path, url)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PlainClone")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(path, url)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockGitClient_PlainClone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PlainClone'
|
||||
type MockGitClient_PlainClone_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// PlainClone is a helper method to define mock.On call
|
||||
// - path string
|
||||
// - url string
|
||||
func (_e *MockGitClient_Expecter) PlainClone(path interface{}, url interface{}) *MockGitClient_PlainClone_Call {
|
||||
return &MockGitClient_PlainClone_Call{Call: _e.mock.On("PlainClone", path, url)}
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_PlainClone_Call) Run(run func(path string, url string)) *MockGitClient_PlainClone_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_PlainClone_Call) Return(_a0 error) *MockGitClient_PlainClone_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_PlainClone_Call) RunAndReturn(run func(string, string) error) *MockGitClient_PlainClone_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Pull provides a mock function with given fields: path
|
||||
func (_m *MockGitClient) Pull(path string) error {
|
||||
ret := _m.Called(path)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Pull")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(path)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockGitClient_Pull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pull'
|
||||
type MockGitClient_Pull_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Pull is a helper method to define mock.On call
|
||||
// - path string
|
||||
func (_e *MockGitClient_Expecter) Pull(path interface{}) *MockGitClient_Pull_Call {
|
||||
return &MockGitClient_Pull_Call{Call: _e.mock.On("Pull", path)}
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_Pull_Call) Run(run func(path string)) *MockGitClient_Pull_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_Pull_Call) Return(_a0 error) *MockGitClient_Pull_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockGitClient_Pull_Call) RunAndReturn(run func(string) error) *MockGitClient_Pull_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockGitClient creates a new instance of MockGitClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockGitClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockGitClient {
|
||||
mock := &MockGitClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
427
internal/mocks/net/mock_Conn.go
Normal file
427
internal/mocks/net/mock_Conn.go
Normal file
@@ -0,0 +1,427 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
net "net"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockConn is an autogenerated mock type for the Conn type
|
||||
type MockConn struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockConn_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockConn) EXPECT() *MockConn_Expecter {
|
||||
return &MockConn_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with no fields
|
||||
func (_m *MockConn) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockConn_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockConn_Expecter) Close() *MockConn_Close_Call {
|
||||
return &MockConn_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockConn_Close_Call) Run(run func()) *MockConn_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Close_Call) Return(_a0 error) *MockConn_Close_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Close_Call) RunAndReturn(run func() error) *MockConn_Close_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// LocalAddr provides a mock function with no fields
|
||||
func (_m *MockConn) LocalAddr() net.Addr {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for LocalAddr")
|
||||
}
|
||||
|
||||
var r0 net.Addr
|
||||
if rf, ok := ret.Get(0).(func() net.Addr); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(net.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_LocalAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LocalAddr'
|
||||
type MockConn_LocalAddr_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// LocalAddr is a helper method to define mock.On call
|
||||
func (_e *MockConn_Expecter) LocalAddr() *MockConn_LocalAddr_Call {
|
||||
return &MockConn_LocalAddr_Call{Call: _e.mock.On("LocalAddr")}
|
||||
}
|
||||
|
||||
func (_c *MockConn_LocalAddr_Call) Run(run func()) *MockConn_LocalAddr_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_LocalAddr_Call) Return(_a0 net.Addr) *MockConn_LocalAddr_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_LocalAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_LocalAddr_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Read provides a mock function with given fields: b
|
||||
func (_m *MockConn) Read(b []byte) (int, error) {
|
||||
ret := _m.Called(b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Read")
|
||||
}
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
|
||||
return rf(b)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func([]byte) int); ok {
|
||||
r0 = rf(b)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConn_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
|
||||
type MockConn_Read_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Read is a helper method to define mock.On call
|
||||
// - b []byte
|
||||
func (_e *MockConn_Expecter) Read(b interface{}) *MockConn_Read_Call {
|
||||
return &MockConn_Read_Call{Call: _e.mock.On("Read", b)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_Read_Call) Run(run func(b []byte)) *MockConn_Read_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Read_Call) Return(n int, err error) *MockConn_Read_Call {
|
||||
_c.Call.Return(n, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Read_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Read_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoteAddr provides a mock function with no fields
|
||||
func (_m *MockConn) RemoteAddr() net.Addr {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoteAddr")
|
||||
}
|
||||
|
||||
var r0 net.Addr
|
||||
if rf, ok := ret.Get(0).(func() net.Addr); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(net.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_RemoteAddr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoteAddr'
|
||||
type MockConn_RemoteAddr_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoteAddr is a helper method to define mock.On call
|
||||
func (_e *MockConn_Expecter) RemoteAddr() *MockConn_RemoteAddr_Call {
|
||||
return &MockConn_RemoteAddr_Call{Call: _e.mock.On("RemoteAddr")}
|
||||
}
|
||||
|
||||
func (_c *MockConn_RemoteAddr_Call) Run(run func()) *MockConn_RemoteAddr_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_RemoteAddr_Call) Return(_a0 net.Addr) *MockConn_RemoteAddr_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_RemoteAddr_Call) RunAndReturn(run func() net.Addr) *MockConn_RemoteAddr_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDeadline provides a mock function with given fields: t
|
||||
func (_m *MockConn) SetDeadline(t time.Time) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetDeadline")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_SetDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDeadline'
|
||||
type MockConn_SetDeadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDeadline is a helper method to define mock.On call
|
||||
// - t time.Time
|
||||
func (_e *MockConn_Expecter) SetDeadline(t interface{}) *MockConn_SetDeadline_Call {
|
||||
return &MockConn_SetDeadline_Call{Call: _e.mock.On("SetDeadline", t)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetDeadline_Call) Run(run func(t time.Time)) *MockConn_SetDeadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetDeadline_Call) Return(_a0 error) *MockConn_SetDeadline_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetDeadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetReadDeadline provides a mock function with given fields: t
|
||||
func (_m *MockConn) SetReadDeadline(t time.Time) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetReadDeadline")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_SetReadDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetReadDeadline'
|
||||
type MockConn_SetReadDeadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetReadDeadline is a helper method to define mock.On call
|
||||
// - t time.Time
|
||||
func (_e *MockConn_Expecter) SetReadDeadline(t interface{}) *MockConn_SetReadDeadline_Call {
|
||||
return &MockConn_SetReadDeadline_Call{Call: _e.mock.On("SetReadDeadline", t)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetReadDeadline_Call) Run(run func(t time.Time)) *MockConn_SetReadDeadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetReadDeadline_Call) Return(_a0 error) *MockConn_SetReadDeadline_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetReadDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetReadDeadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetWriteDeadline provides a mock function with given fields: t
|
||||
func (_m *MockConn) SetWriteDeadline(t time.Time) error {
|
||||
ret := _m.Called(t)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetWriteDeadline")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(time.Time) error); ok {
|
||||
r0 = rf(t)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockConn_SetWriteDeadline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWriteDeadline'
|
||||
type MockConn_SetWriteDeadline_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetWriteDeadline is a helper method to define mock.On call
|
||||
// - t time.Time
|
||||
func (_e *MockConn_Expecter) SetWriteDeadline(t interface{}) *MockConn_SetWriteDeadline_Call {
|
||||
return &MockConn_SetWriteDeadline_Call{Call: _e.mock.On("SetWriteDeadline", t)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetWriteDeadline_Call) Run(run func(t time.Time)) *MockConn_SetWriteDeadline_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(time.Time))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetWriteDeadline_Call) Return(_a0 error) *MockConn_SetWriteDeadline_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_SetWriteDeadline_Call) RunAndReturn(run func(time.Time) error) *MockConn_SetWriteDeadline_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Write provides a mock function with given fields: b
|
||||
func (_m *MockConn) Write(b []byte) (int, error) {
|
||||
ret := _m.Called(b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Write")
|
||||
}
|
||||
|
||||
var r0 int
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
|
||||
return rf(b)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func([]byte) int); ok {
|
||||
r0 = rf(b)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockConn_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
|
||||
type MockConn_Write_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Write is a helper method to define mock.On call
|
||||
// - b []byte
|
||||
func (_e *MockConn_Expecter) Write(b interface{}) *MockConn_Write_Call {
|
||||
return &MockConn_Write_Call{Call: _e.mock.On("Write", b)}
|
||||
}
|
||||
|
||||
func (_c *MockConn_Write_Call) Run(run func(b []byte)) *MockConn_Write_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].([]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Write_Call) Return(n int, err error) *MockConn_Write_Call {
|
||||
_c.Call.Return(n, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockConn_Write_Call) RunAndReturn(run func([]byte) (int, error)) *MockConn_Write_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockConn creates a new instance of MockConn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockConn(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockConn {
|
||||
mock := &MockConn{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
1371
internal/mocks/network/mock_Backend.go
Normal file
1371
internal/mocks/network/mock_Backend.go
Normal file
File diff suppressed because it is too large
Load Diff
430
internal/plugins/manager.go
Normal file
430
internal/plugins/manager.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
fs afero.Fs
|
||||
pluginsDir string
|
||||
gitClient GitClient
|
||||
}
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
return NewManagerWithFs(afero.NewOsFs())
|
||||
}
|
||||
|
||||
func NewManagerWithFs(fs afero.Fs) (*Manager, error) {
|
||||
pluginsDir := getPluginsDir()
|
||||
return &Manager{
|
||||
fs: fs,
|
||||
pluginsDir: pluginsDir,
|
||||
gitClient: &realGitClient{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getPluginsDir() string {
|
||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if configHome == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return filepath.Join(os.TempDir(), "DankMaterialShell", "plugins")
|
||||
}
|
||||
configHome = filepath.Join(homeDir, ".config")
|
||||
}
|
||||
return filepath.Join(configHome, "DankMaterialShell", "plugins")
|
||||
}
|
||||
|
||||
func (m *Manager) IsInstalled(plugin Plugin) (bool, error) {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if exists {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return systemExists, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Install(plugin Plugin) error {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
if err := m.fs.MkdirAll(m.pluginsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
||||
}
|
||||
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
if err := m.fs.MkdirAll(reposDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create repos directory: %w", err)
|
||||
}
|
||||
|
||||
if plugin.Path != "" {
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
repoExists, err := afero.DirExists(m.fs, repoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if repo exists: %w", err)
|
||||
}
|
||||
|
||||
if !repoExists {
|
||||
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
|
||||
m.fs.RemoveAll(repoPath)
|
||||
return fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Pull latest changes if repo already exists
|
||||
if err := m.gitClient.Pull(repoPath); err != nil {
|
||||
// If pull fails (e.g., corrupted shallow clone), delete and re-clone
|
||||
if err := m.fs.RemoveAll(repoPath); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted repository: %w", err)
|
||||
}
|
||||
|
||||
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone repository: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourcePath := filepath.Join(repoPath, plugin.Path)
|
||||
sourceExists, err := afero.DirExists(m.fs, sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check plugin path: %w", err)
|
||||
}
|
||||
if !sourceExists {
|
||||
return fmt.Errorf("plugin path does not exist in repository: %s", plugin.Path)
|
||||
}
|
||||
|
||||
if err := m.createSymlink(sourcePath, pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to create symlink: %w", err)
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
||||
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write metadata: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil {
|
||||
m.fs.RemoveAll(pluginPath)
|
||||
return fmt.Errorf("failed to clone plugin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getRepoName(repoURL string) string {
|
||||
hash := sha256.Sum256([]byte(repoURL))
|
||||
return hex.EncodeToString(hash[:])[:16]
|
||||
}
|
||||
|
||||
func (m *Manager) createSymlink(source, dest string) error {
|
||||
if symlinkFs, ok := m.fs.(afero.Symlinker); ok {
|
||||
return symlinkFs.SymlinkIfPossible(source, dest)
|
||||
}
|
||||
return os.Symlink(source, dest)
|
||||
}
|
||||
|
||||
func (m *Manager) Update(plugin Plugin) error {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
if systemExists {
|
||||
return fmt.Errorf("cannot update system plugin: %s", plugin.Name)
|
||||
}
|
||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check metadata: %w", err)
|
||||
}
|
||||
|
||||
if metaExists {
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
// Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone
|
||||
if err := m.gitClient.Pull(repoPath); err != nil {
|
||||
// Repository is likely corrupted or has issues, delete and re-clone
|
||||
if err := m.fs.RemoveAll(repoPath); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted repository: %w", err)
|
||||
}
|
||||
|
||||
if err := m.gitClient.PlainClone(repoPath, plugin.Repo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone repository: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to pull, if it fails, delete and re-clone
|
||||
if err := m.gitClient.Pull(pluginPath); err != nil {
|
||||
if err := m.fs.RemoveAll(pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted plugin: %w", err)
|
||||
}
|
||||
|
||||
if err := m.gitClient.PlainClone(pluginPath, plugin.Repo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone plugin: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Uninstall(plugin Plugin) error {
|
||||
pluginPath := filepath.Join(m.pluginsDir, plugin.ID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", plugin.ID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
if systemExists {
|
||||
return fmt.Errorf("cannot uninstall system plugin: %s", plugin.Name)
|
||||
}
|
||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check metadata: %w", err)
|
||||
}
|
||||
|
||||
if metaExists {
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
shouldCleanup, err := m.shouldCleanupRepo(repoPath, plugin.Repo, plugin.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check repo cleanup: %w", err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to remove symlink: %w", err)
|
||||
}
|
||||
|
||||
if err := m.fs.Remove(metaPath); err != nil {
|
||||
return fmt.Errorf("failed to remove metadata: %w", err)
|
||||
}
|
||||
|
||||
if shouldCleanup {
|
||||
if err := m.fs.RemoveAll(repoPath); err != nil {
|
||||
return fmt.Errorf("failed to cleanup repository: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := m.fs.RemoveAll(pluginPath); err != nil {
|
||||
return fmt.Errorf("failed to remove plugin: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) shouldCleanupRepo(repoPath, repoURL, excludePlugin string) (bool, error) {
|
||||
installed, err := m.ListInstalled()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
registry, err := NewRegistry()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
allPlugins, err := registry.List()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, id := range installed {
|
||||
if id == excludePlugin {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range allPlugins {
|
||||
if p.ID == id && p.Repo == repoURL && p.Path != "" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListInstalled() ([]string, error) {
|
||||
installedMap := make(map[string]bool)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, m.pluginsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
entries, err := afero.ReadDir(m.fs, m.pluginsDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read plugins directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == ".repos" || strings.HasSuffix(name, ".meta") {
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(m.pluginsDir, name)
|
||||
isPlugin := false
|
||||
|
||||
if entry.IsDir() {
|
||||
isPlugin = true
|
||||
} else if entry.Mode()&os.ModeSymlink != 0 {
|
||||
isPlugin = true
|
||||
} else {
|
||||
info, err := m.fs.Stat(fullPath)
|
||||
if err == nil && info.IsDir() {
|
||||
isPlugin = true
|
||||
}
|
||||
}
|
||||
|
||||
if isPlugin {
|
||||
// Read plugin.json to get the actual plugin ID
|
||||
pluginID := m.getPluginID(fullPath)
|
||||
if pluginID != "" {
|
||||
installedMap[pluginID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
systemPluginsDir := "/etc/xdg/quickshell/dms-plugins"
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginsDir)
|
||||
if err == nil && systemExists {
|
||||
entries, err := afero.ReadDir(m.fs, systemPluginsDir)
|
||||
if err == nil {
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
fullPath := filepath.Join(systemPluginsDir, entry.Name())
|
||||
// Read plugin.json to get the actual plugin ID
|
||||
pluginID := m.getPluginID(fullPath)
|
||||
if pluginID != "" {
|
||||
installedMap[pluginID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var installed []string
|
||||
for name := range installedMap {
|
||||
installed = append(installed, name)
|
||||
}
|
||||
|
||||
return installed, nil
|
||||
}
|
||||
|
||||
// getPluginID reads the plugin.json file and returns the plugin ID
|
||||
func (m *Manager) getPluginID(pluginPath string) string {
|
||||
manifestPath := filepath.Join(pluginPath, "plugin.json")
|
||||
data, err := afero.ReadFile(m.fs, manifestPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var manifest struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return manifest.ID
|
||||
}
|
||||
|
||||
func (m *Manager) GetPluginsDir() string {
|
||||
return m.pluginsDir
|
||||
}
|
||||
|
||||
func (m *Manager) HasUpdates(pluginID string, plugin Plugin) (bool, error) {
|
||||
pluginPath := filepath.Join(m.pluginsDir, pluginID)
|
||||
|
||||
exists, err := afero.DirExists(m.fs, pluginPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check if plugin exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
systemPluginPath := filepath.Join("/etc/xdg/quickshell/dms-plugins", pluginID)
|
||||
systemExists, err := afero.DirExists(m.fs, systemPluginPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check system plugin: %w", err)
|
||||
}
|
||||
if systemExists {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("plugin not installed: %s", pluginID)
|
||||
}
|
||||
|
||||
// Check if there's a .meta file (plugin installed from a monorepo)
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaExists, err := afero.Exists(m.fs, metaPath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check metadata: %w", err)
|
||||
}
|
||||
|
||||
if metaExists {
|
||||
// Plugin is from a monorepo, check the repo directory
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
repoName := m.getRepoName(plugin.Repo)
|
||||
repoPath := filepath.Join(reposDir, repoName)
|
||||
|
||||
return m.gitClient.HasUpdates(repoPath)
|
||||
}
|
||||
|
||||
// Plugin is a standalone repo
|
||||
return m.gitClient.HasUpdates(pluginPath)
|
||||
}
|
||||
247
internal/plugins/manager_test.go
Normal file
247
internal/plugins/manager_test.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestManager(t *testing.T) (*Manager, afero.Fs, string) {
|
||||
fs := afero.NewMemMapFs()
|
||||
pluginsDir := "/test-plugins"
|
||||
manager := &Manager{
|
||||
fs: fs,
|
||||
pluginsDir: pluginsDir,
|
||||
gitClient: &mockGitClient{},
|
||||
}
|
||||
return manager, fs, pluginsDir
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
manager, err := NewManager()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, manager)
|
||||
assert.NotEmpty(t, manager.pluginsDir)
|
||||
}
|
||||
|
||||
func TestGetPluginsDir(t *testing.T) {
|
||||
t.Run("uses XDG_CONFIG_HOME when set", func(t *testing.T) {
|
||||
oldConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
defer func() {
|
||||
if oldConfig != "" {
|
||||
os.Setenv("XDG_CONFIG_HOME", oldConfig)
|
||||
} else {
|
||||
os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
}()
|
||||
|
||||
os.Setenv("XDG_CONFIG_HOME", "/tmp/test-config")
|
||||
dir := getPluginsDir()
|
||||
assert.Equal(t, "/tmp/test-config/DankMaterialShell/plugins", dir)
|
||||
})
|
||||
|
||||
t.Run("falls back to home directory", func(t *testing.T) {
|
||||
oldConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
defer func() {
|
||||
if oldConfig != "" {
|
||||
os.Setenv("XDG_CONFIG_HOME", oldConfig)
|
||||
} else {
|
||||
os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
}()
|
||||
|
||||
os.Unsetenv("XDG_CONFIG_HOME")
|
||||
dir := getPluginsDir()
|
||||
assert.Contains(t, dir, ".config/DankMaterialShell/plugins")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsInstalled(t *testing.T) {
|
||||
t.Run("returns true when plugin is installed", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
installed, err := manager.IsInstalled(plugin)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, installed)
|
||||
})
|
||||
|
||||
t.Run("returns false when plugin is not installed", func(t *testing.T) {
|
||||
manager, _, _ := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "non-existent", Name: "NonExistent"}
|
||||
installed, err := manager.IsInstalled(plugin)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, installed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstall(t *testing.T) {
|
||||
t.Run("installs plugin successfully", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{
|
||||
ID: "test-plugin",
|
||||
Name: "TestPlugin",
|
||||
Repo: "https://github.com/test/plugin",
|
||||
}
|
||||
|
||||
cloneCalled := false
|
||||
mockGit := &mockGitClient{
|
||||
cloneFunc: func(path string, url string) error {
|
||||
cloneCalled = true
|
||||
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
||||
assert.Equal(t, plugin.Repo, url)
|
||||
return fs.MkdirAll(path, 0755)
|
||||
},
|
||||
}
|
||||
manager.gitClient = mockGit
|
||||
|
||||
err := manager.Install(plugin)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, cloneCalled)
|
||||
|
||||
exists, _ := afero.DirExists(fs, filepath.Join(pluginsDir, plugin.ID))
|
||||
assert.True(t, exists)
|
||||
})
|
||||
|
||||
t.Run("returns error when plugin already installed", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager.Install(plugin)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already installed")
|
||||
})
|
||||
|
||||
t.Run("installs monorepo plugin with symlink", func(t *testing.T) {
|
||||
t.Skip("Skipping symlink test as MemMapFs doesn't support symlinks")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManagerUpdate(t *testing.T) {
|
||||
t.Run("updates plugin successfully", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
pullCalled := false
|
||||
mockGit := &mockGitClient{
|
||||
pullFunc: func(path string) error {
|
||||
pullCalled = true
|
||||
assert.Equal(t, pluginPath, path)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
manager.gitClient = mockGit
|
||||
|
||||
err = manager.Update(plugin)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pullCalled)
|
||||
})
|
||||
|
||||
t.Run("returns error when plugin not installed", func(t *testing.T) {
|
||||
manager, _, _ := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "non-existent", Name: "NonExistent"}
|
||||
err := manager.Update(plugin)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not installed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUninstall(t *testing.T) {
|
||||
t.Run("uninstalls plugin successfully", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager.Uninstall(plugin)
|
||||
assert.NoError(t, err)
|
||||
|
||||
exists, _ := afero.DirExists(fs, pluginPath)
|
||||
assert.False(t, exists)
|
||||
})
|
||||
|
||||
t.Run("returns error when plugin not installed", func(t *testing.T) {
|
||||
manager, _, _ := setupTestManager(t)
|
||||
|
||||
plugin := Plugin{ID: "non-existent", Name: "NonExistent"}
|
||||
err := manager.Uninstall(plugin)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not installed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestListInstalled(t *testing.T) {
|
||||
t.Run("lists installed plugins", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
installed, err := manager.ListInstalled()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, installed, 2)
|
||||
assert.Contains(t, installed, "Plugin1")
|
||||
assert.Contains(t, installed, "Plugin2")
|
||||
})
|
||||
|
||||
t.Run("returns empty list when no plugins installed", func(t *testing.T) {
|
||||
manager, _, _ := setupTestManager(t)
|
||||
|
||||
installed, err := manager.ListInstalled()
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, installed)
|
||||
})
|
||||
|
||||
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
||||
require.NoError(t, err)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
installed, err := manager.ListInstalled()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, installed, 1)
|
||||
assert.Equal(t, "Plugin1", installed[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestManagerGetPluginsDir(t *testing.T) {
|
||||
manager, _, pluginsDir := setupTestManager(t)
|
||||
assert.Equal(t, pluginsDir, manager.GetPluginsDir())
|
||||
}
|
||||
256
internal/plugins/registry.go
Normal file
256
internal/plugins/registry.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v6"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
const registryRepo = "https://github.com/AvengeMedia/dms-plugin-registry.git"
|
||||
|
||||
type Plugin struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Category string `json:"category"`
|
||||
Repo string `json:"repo"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Author string `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
Compositors []string `json:"compositors"`
|
||||
Distro []string `json:"distro"`
|
||||
Screenshot string `json:"screenshot,omitempty"`
|
||||
}
|
||||
|
||||
type GitClient interface {
|
||||
PlainClone(path string, url string) error
|
||||
Pull(path string) error
|
||||
HasUpdates(path string) (bool, error)
|
||||
}
|
||||
|
||||
type realGitClient struct{}
|
||||
|
||||
func (g *realGitClient) PlainClone(path string, url string) error {
|
||||
_, err := git.PlainClone(path, &git.CloneOptions{
|
||||
URL: url,
|
||||
Progress: os.Stdout,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *realGitClient) Pull(path string) error {
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = worktree.Pull(&git.PullOptions{})
|
||||
if err != nil && err.Error() != "already up-to-date" {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *realGitClient) HasUpdates(path string) (bool, error) {
|
||||
repo, err := git.PlainOpen(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Fetch remote changes
|
||||
err = repo.Fetch(&git.FetchOptions{})
|
||||
if err != nil && err.Error() != "already up-to-date" {
|
||||
// If fetch fails, we can't determine if there are updates
|
||||
// Return false and the error
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Get the HEAD reference
|
||||
head, err := repo.Head()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Get the remote HEAD reference (typically origin/HEAD or origin/main or origin/master)
|
||||
remote, err := repo.Remote("origin")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
refs, err := remote.List(&git.ListOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Find the default branch remote ref
|
||||
var remoteHead string
|
||||
for _, ref := range refs {
|
||||
if ref.Name().IsBranch() {
|
||||
// Try common branch names
|
||||
if ref.Name().Short() == "main" || ref.Name().Short() == "master" {
|
||||
remoteHead = ref.Hash().String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't find a remote HEAD, assume no updates
|
||||
if remoteHead == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Compare local HEAD with remote HEAD
|
||||
return head.Hash().String() != remoteHead, nil
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
fs afero.Fs
|
||||
cacheDir string
|
||||
plugins []Plugin
|
||||
git GitClient
|
||||
}
|
||||
|
||||
func NewRegistry() (*Registry, error) {
|
||||
return NewRegistryWithFs(afero.NewOsFs())
|
||||
}
|
||||
|
||||
func NewRegistryWithFs(fs afero.Fs) (*Registry, error) {
|
||||
cacheDir := getCacheDir()
|
||||
return &Registry{
|
||||
fs: fs,
|
||||
cacheDir: cacheDir,
|
||||
git: &realGitClient{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getCacheDir() string {
|
||||
return filepath.Join(os.TempDir(), "dankdots-plugin-registry")
|
||||
}
|
||||
|
||||
func (r *Registry) Update() error {
|
||||
exists, err := afero.DirExists(r.fs, r.cacheDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check cache directory: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
|
||||
return fmt.Errorf("failed to clone registry: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Try to pull, if it fails (e.g., shallow clone corruption), delete and re-clone
|
||||
if err := r.git.Pull(r.cacheDir); err != nil {
|
||||
// Repository is likely corrupted or has issues, delete and re-clone
|
||||
if err := r.fs.RemoveAll(r.cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
||||
}
|
||||
|
||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
if err := r.git.PlainClone(r.cacheDir, registryRepo); err != nil {
|
||||
return fmt.Errorf("failed to re-clone registry: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return r.loadPlugins()
|
||||
}
|
||||
|
||||
func (r *Registry) loadPlugins() error {
|
||||
pluginsDir := filepath.Join(r.cacheDir, "plugins")
|
||||
|
||||
entries, err := afero.ReadDir(r.fs, pluginsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read plugins directory: %w", err)
|
||||
}
|
||||
|
||||
r.plugins = []Plugin{}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := afero.ReadFile(r.fs, filepath.Join(pluginsDir, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var plugin Plugin
|
||||
if err := json.Unmarshal(data, &plugin); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if plugin.ID == "" {
|
||||
plugin.ID = strings.TrimSuffix(entry.Name(), ".json")
|
||||
}
|
||||
|
||||
r.plugins = append(r.plugins, plugin)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) List() ([]Plugin, error) {
|
||||
if len(r.plugins) == 0 {
|
||||
if err := r.Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return SortByFirstParty(r.plugins), nil
|
||||
}
|
||||
|
||||
func (r *Registry) Search(query string) ([]Plugin, error) {
|
||||
allPlugins, err := r.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
return allPlugins, nil
|
||||
}
|
||||
|
||||
return SortByFirstParty(FuzzySearch(query, allPlugins)), nil
|
||||
}
|
||||
|
||||
func (r *Registry) Get(idOrName string) (*Plugin, error) {
|
||||
plugins, err := r.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First, try to find by ID (preferred method)
|
||||
for _, p := range plugins {
|
||||
if p.ID == idOrName {
|
||||
return &p, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to name for backward compatibility
|
||||
for _, p := range plugins {
|
||||
if p.Name == idOrName {
|
||||
return &p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("plugin not found: %s", idOrName)
|
||||
}
|
||||
326
internal/plugins/registry_test.go
Normal file
326
internal/plugins/registry_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockGitClient struct {
|
||||
cloneFunc func(path string, url string) error
|
||||
pullFunc func(path string) error
|
||||
hasUpdatesFunc func(path string) (bool, error)
|
||||
}
|
||||
|
||||
func (m *mockGitClient) PlainClone(path string, url string) error {
|
||||
if m.cloneFunc != nil {
|
||||
return m.cloneFunc(path, url)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockGitClient) Pull(path string) error {
|
||||
if m.pullFunc != nil {
|
||||
return m.pullFunc(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockGitClient) HasUpdates(path string) (bool, error) {
|
||||
if m.hasUpdatesFunc != nil {
|
||||
return m.hasUpdatesFunc(path)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func TestNewRegistry(t *testing.T) {
|
||||
registry, err := NewRegistry()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, registry)
|
||||
assert.NotEmpty(t, registry.cacheDir)
|
||||
}
|
||||
|
||||
func TestGetCacheDir(t *testing.T) {
|
||||
cacheDir := getCacheDir()
|
||||
assert.Contains(t, cacheDir, "/tmp/dankdots-plugin-registry")
|
||||
}
|
||||
|
||||
func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) {
|
||||
fs := afero.NewMemMapFs()
|
||||
tmpDir := "/test-cache"
|
||||
registry := &Registry{
|
||||
fs: fs,
|
||||
cacheDir: tmpDir,
|
||||
plugins: []Plugin{},
|
||||
git: &mockGitClient{},
|
||||
}
|
||||
return registry, fs, tmpDir
|
||||
}
|
||||
|
||||
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
||||
pluginsDir := filepath.Join(dir, "plugins")
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := json.Marshal(plugin)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0644)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoadPlugins(t *testing.T) {
|
||||
t.Run("loads valid plugin files", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
plugin1 := Plugin{
|
||||
Name: "TestPlugin1",
|
||||
Capabilities: []string{"dankbar-widget"},
|
||||
Category: "monitoring",
|
||||
Repo: "https://github.com/test/plugin1",
|
||||
Author: "Test Author",
|
||||
Description: "Test plugin 1",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
|
||||
plugin2 := Plugin{
|
||||
Name: "TestPlugin2",
|
||||
Capabilities: []string{"system-tray"},
|
||||
Category: "utilities",
|
||||
Repo: "https://github.com/test/plugin2",
|
||||
Author: "Another Author",
|
||||
Description: "Test plugin 2",
|
||||
Dependencies: []string{"dep1", "dep2"},
|
||||
Compositors: []string{"hyprland", "niri"},
|
||||
Distro: []string{"arch"},
|
||||
Screenshot: "https://example.com/screenshot.png",
|
||||
}
|
||||
|
||||
createTestPlugin(t, fs, tmpDir, "plugin1.json", plugin1)
|
||||
createTestPlugin(t, fs, tmpDir, "plugin2.json", plugin2)
|
||||
|
||||
err := registry.loadPlugins()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, registry.plugins, 2)
|
||||
|
||||
assert.Equal(t, "TestPlugin1", registry.plugins[0].Name)
|
||||
assert.Equal(t, "TestPlugin2", registry.plugins[1].Name)
|
||||
assert.Equal(t, []string{"dankbar-widget"}, registry.plugins[0].Capabilities)
|
||||
assert.Equal(t, []string{"dep1", "dep2"}, registry.plugins[1].Dependencies)
|
||||
})
|
||||
|
||||
t.Run("skips non-json files", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "ValidPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
createTestPlugin(t, fs, tmpDir, "valid.json", plugin)
|
||||
|
||||
err = registry.loadPlugins()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, registry.plugins, 1)
|
||||
assert.Equal(t, "ValidPlugin", registry.plugins[0].Name)
|
||||
})
|
||||
|
||||
t.Run("skips directories", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "ValidPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
createTestPlugin(t, fs, tmpDir, "valid.json", plugin)
|
||||
|
||||
err = registry.loadPlugins()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, registry.plugins, 1)
|
||||
})
|
||||
|
||||
t.Run("skips invalid json files", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "ValidPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
createTestPlugin(t, fs, tmpDir, "valid.json", plugin)
|
||||
|
||||
err = registry.loadPlugins()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, registry.plugins, 1)
|
||||
assert.Equal(t, "ValidPlugin", registry.plugins[0].Name)
|
||||
})
|
||||
|
||||
t.Run("returns error when plugins directory missing", func(t *testing.T) {
|
||||
registry, _, _ := setupTestRegistry(t)
|
||||
|
||||
err := registry.loadPlugins()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to read plugins directory")
|
||||
})
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
t.Run("returns cached plugins if available", func(t *testing.T) {
|
||||
registry, _, _ := setupTestRegistry(t)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "CachedPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
|
||||
registry.plugins = []Plugin{plugin}
|
||||
|
||||
plugins, err := registry.List()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, plugins, 1)
|
||||
assert.Equal(t, "CachedPlugin", plugins[0].Name)
|
||||
})
|
||||
|
||||
t.Run("updates and loads plugins when cache is empty", func(t *testing.T) {
|
||||
registry, fs, _ := setupTestRegistry(t)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "NewPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
|
||||
mockGit := &mockGitClient{
|
||||
cloneFunc: func(path string, url string) error {
|
||||
createTestPlugin(t, fs, path, "plugin.json", plugin)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
registry.git = mockGit
|
||||
|
||||
plugins, err := registry.List()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, plugins, 1)
|
||||
assert.Equal(t, "NewPlugin", plugins[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Run("clones repository when cache doesn't exist", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "RepoPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
|
||||
cloneCalled := false
|
||||
mockGit := &mockGitClient{
|
||||
cloneFunc: func(path string, url string) error {
|
||||
cloneCalled = true
|
||||
assert.Equal(t, registryRepo, url)
|
||||
assert.Equal(t, tmpDir, path)
|
||||
createTestPlugin(t, fs, path, "plugin.json", plugin)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
registry.git = mockGit
|
||||
|
||||
err := registry.Update()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, cloneCalled)
|
||||
assert.Len(t, registry.plugins, 1)
|
||||
assert.Equal(t, "RepoPlugin", registry.plugins[0].Name)
|
||||
})
|
||||
|
||||
t.Run("pulls updates when cache exists", func(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
plugin := Plugin{
|
||||
Name: "UpdatedPlugin",
|
||||
Capabilities: []string{"test"},
|
||||
Category: "test",
|
||||
Repo: "https://github.com/test/test",
|
||||
Author: "Test",
|
||||
Description: "Test",
|
||||
Compositors: []string{"niri"},
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
|
||||
err := fs.MkdirAll(tmpDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
pullCalled := false
|
||||
mockGit := &mockGitClient{
|
||||
pullFunc: func(path string) error {
|
||||
pullCalled = true
|
||||
assert.Equal(t, tmpDir, path)
|
||||
createTestPlugin(t, fs, path, "plugin.json", plugin)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
registry.git = mockGit
|
||||
|
||||
err = registry.Update()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, pullCalled)
|
||||
assert.Len(t, registry.plugins, 1)
|
||||
assert.Equal(t, "UpdatedPlugin", registry.plugins[0].Name)
|
||||
})
|
||||
}
|
||||
105
internal/plugins/search.go
Normal file
105
internal/plugins/search.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FuzzySearch(query string, plugins []Plugin) []Plugin {
|
||||
if query == "" {
|
||||
return plugins
|
||||
}
|
||||
|
||||
queryLower := strings.ToLower(query)
|
||||
var results []Plugin
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if fuzzyMatch(queryLower, strings.ToLower(plugin.Name)) ||
|
||||
fuzzyMatch(queryLower, strings.ToLower(plugin.Category)) ||
|
||||
fuzzyMatch(queryLower, strings.ToLower(plugin.Description)) ||
|
||||
fuzzyMatch(queryLower, strings.ToLower(plugin.Author)) {
|
||||
results = append(results, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func fuzzyMatch(query, text string) bool {
|
||||
queryIdx := 0
|
||||
for _, char := range text {
|
||||
if queryIdx < len(query) && char == rune(query[queryIdx]) {
|
||||
queryIdx++
|
||||
}
|
||||
}
|
||||
return queryIdx == len(query)
|
||||
}
|
||||
|
||||
func FilterByCategory(category string, plugins []Plugin) []Plugin {
|
||||
if category == "" {
|
||||
return plugins
|
||||
}
|
||||
|
||||
var results []Plugin
|
||||
categoryLower := strings.ToLower(category)
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if strings.ToLower(plugin.Category) == categoryLower {
|
||||
results = append(results, plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func FilterByCompositor(compositor string, plugins []Plugin) []Plugin {
|
||||
if compositor == "" {
|
||||
return plugins
|
||||
}
|
||||
|
||||
var results []Plugin
|
||||
compositorLower := strings.ToLower(compositor)
|
||||
|
||||
for _, plugin := range plugins {
|
||||
for _, comp := range plugin.Compositors {
|
||||
if strings.ToLower(comp) == compositorLower {
|
||||
results = append(results, plugin)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func FilterByCapability(capability string, plugins []Plugin) []Plugin {
|
||||
if capability == "" {
|
||||
return plugins
|
||||
}
|
||||
|
||||
var results []Plugin
|
||||
capabilityLower := strings.ToLower(capability)
|
||||
|
||||
for _, plugin := range plugins {
|
||||
for _, cap := range plugin.Capabilities {
|
||||
if strings.ToLower(cap) == capabilityLower {
|
||||
results = append(results, plugin)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func SortByFirstParty(plugins []Plugin) []Plugin {
|
||||
sort.SliceStable(plugins, func(i, j int) bool {
|
||||
isFirstPartyI := strings.HasPrefix(plugins[i].Repo, "https://github.com/AvengeMedia")
|
||||
isFirstPartyJ := strings.HasPrefix(plugins[j].Repo, "https://github.com/AvengeMedia")
|
||||
if isFirstPartyI != isFirstPartyJ {
|
||||
return isFirstPartyI
|
||||
}
|
||||
return false
|
||||
})
|
||||
return plugins
|
||||
}
|
||||
491
internal/proto/dwl_ipc/dwl_ipc.go
Normal file
491
internal/proto/dwl_ipc/dwl_ipc.go
Normal file
@@ -0,0 +1,491 @@
|
||||
// Generated by go-wayland-scanner
|
||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||
// XML file : internal/proto/xml/dwl-ipc-unstable-v2.xml
|
||||
//
|
||||
// dwl_ipc_unstable_v2 Protocol Copyright:
|
||||
|
||||
package dwl_ipc
|
||||
|
||||
import "github.com/yaslama/go-wayland/wayland/client"
|
||||
|
||||
// ZdwlIpcManagerV2InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZdwlIpcManagerV2InterfaceName = "zdwl_ipc_manager_v2"
|
||||
|
||||
// ZdwlIpcManagerV2 : manage dwl state
|
||||
//
|
||||
// This interface is exposed as a global in wl_registry.
|
||||
//
|
||||
// Clients can use this interface to get a dwl_ipc_output.
|
||||
// After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
|
||||
// The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
|
||||
type ZdwlIpcManagerV2 struct {
|
||||
client.BaseProxy
|
||||
tagsHandler ZdwlIpcManagerV2TagsHandlerFunc
|
||||
layoutHandler ZdwlIpcManagerV2LayoutHandlerFunc
|
||||
}
|
||||
|
||||
// NewZdwlIpcManagerV2 : manage dwl state
|
||||
//
|
||||
// This interface is exposed as a global in wl_registry.
|
||||
//
|
||||
// Clients can use this interface to get a dwl_ipc_output.
|
||||
// After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
|
||||
// The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
|
||||
func NewZdwlIpcManagerV2(ctx *client.Context) *ZdwlIpcManagerV2 {
|
||||
zdwlIpcManagerV2 := &ZdwlIpcManagerV2{}
|
||||
ctx.Register(zdwlIpcManagerV2)
|
||||
return zdwlIpcManagerV2
|
||||
}
|
||||
|
||||
// Release : release dwl_ipc_manager
|
||||
//
|
||||
// Indicates that the client will not the dwl_ipc_manager object anymore.
|
||||
// Objects created through this instance are not affected.
|
||||
func (i *ZdwlIpcManagerV2) Release() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOutput : get a dwl_ipc_outout for a wl_output
|
||||
//
|
||||
// Get a dwl_ipc_outout for the specified wl_output.
|
||||
func (i *ZdwlIpcManagerV2) GetOutput(output *client.Output) (*ZdwlIpcOutputV2, error) {
|
||||
id := NewZdwlIpcOutputV2(i.Context())
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// ZdwlIpcManagerV2TagsEvent : Announces tag amount
|
||||
//
|
||||
// This event is sent after binding.
|
||||
// A roundtrip after binding guarantees the client recieved all tags.
|
||||
type ZdwlIpcManagerV2TagsEvent struct {
|
||||
Amount uint32
|
||||
}
|
||||
type ZdwlIpcManagerV2TagsHandlerFunc func(ZdwlIpcManagerV2TagsEvent)
|
||||
|
||||
// SetTagsHandler : sets handler for ZdwlIpcManagerV2TagsEvent
|
||||
func (i *ZdwlIpcManagerV2) SetTagsHandler(f ZdwlIpcManagerV2TagsHandlerFunc) {
|
||||
i.tagsHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcManagerV2LayoutEvent : Announces a layout
|
||||
//
|
||||
// This event is sent after binding.
|
||||
// A roundtrip after binding guarantees the client recieved all layouts.
|
||||
type ZdwlIpcManagerV2LayoutEvent struct {
|
||||
Name string
|
||||
}
|
||||
type ZdwlIpcManagerV2LayoutHandlerFunc func(ZdwlIpcManagerV2LayoutEvent)
|
||||
|
||||
// SetLayoutHandler : sets handler for ZdwlIpcManagerV2LayoutEvent
|
||||
func (i *ZdwlIpcManagerV2) SetLayoutHandler(f ZdwlIpcManagerV2LayoutHandlerFunc) {
|
||||
i.layoutHandler = f
|
||||
}
|
||||
|
||||
func (i *ZdwlIpcManagerV2) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if i.tagsHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcManagerV2TagsEvent
|
||||
l := 0
|
||||
e.Amount = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.tagsHandler(e)
|
||||
case 1:
|
||||
if i.layoutHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcManagerV2LayoutEvent
|
||||
l := 0
|
||||
nameLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
||||
l += 4
|
||||
e.Name = client.String(data[l : l+nameLen])
|
||||
l += nameLen
|
||||
|
||||
i.layoutHandler(e)
|
||||
}
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZdwlIpcOutputV2InterfaceName = "zdwl_ipc_output_v2"
|
||||
|
||||
// ZdwlIpcOutputV2 : control dwl output
|
||||
//
|
||||
// Observe and control a dwl output.
|
||||
//
|
||||
// Events are double-buffered:
|
||||
// Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
|
||||
//
|
||||
// Request are not double-buffered:
|
||||
// The compositor will update immediately upon request.
|
||||
type ZdwlIpcOutputV2 struct {
|
||||
client.BaseProxy
|
||||
toggleVisibilityHandler ZdwlIpcOutputV2ToggleVisibilityHandlerFunc
|
||||
activeHandler ZdwlIpcOutputV2ActiveHandlerFunc
|
||||
tagHandler ZdwlIpcOutputV2TagHandlerFunc
|
||||
layoutHandler ZdwlIpcOutputV2LayoutHandlerFunc
|
||||
titleHandler ZdwlIpcOutputV2TitleHandlerFunc
|
||||
appidHandler ZdwlIpcOutputV2AppidHandlerFunc
|
||||
layoutSymbolHandler ZdwlIpcOutputV2LayoutSymbolHandlerFunc
|
||||
frameHandler ZdwlIpcOutputV2FrameHandlerFunc
|
||||
}
|
||||
|
||||
// NewZdwlIpcOutputV2 : control dwl output
|
||||
//
|
||||
// Observe and control a dwl output.
|
||||
//
|
||||
// Events are double-buffered:
|
||||
// Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
|
||||
//
|
||||
// Request are not double-buffered:
|
||||
// The compositor will update immediately upon request.
|
||||
func NewZdwlIpcOutputV2(ctx *client.Context) *ZdwlIpcOutputV2 {
|
||||
zdwlIpcOutputV2 := &ZdwlIpcOutputV2{}
|
||||
ctx.Register(zdwlIpcOutputV2)
|
||||
return zdwlIpcOutputV2
|
||||
}
|
||||
|
||||
// Release : release dwl_ipc_outout
|
||||
//
|
||||
// Indicates to that the client no longer needs this dwl_ipc_output.
|
||||
func (i *ZdwlIpcOutputV2) Release() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetTags : Set the active tags of this output
|
||||
//
|
||||
// tagmask: bitmask of the tags that should be set.
|
||||
// toggleTagset: toggle the selected tagset, zero for invalid, nonzero for valid.
|
||||
func (i *ZdwlIpcOutputV2) SetTags(tagmask, toggleTagset uint32) error {
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(tagmask))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(toggleTagset))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetClientTags : Set the tags of the focused client.
|
||||
//
|
||||
// The tags are updated as follows:
|
||||
// new_tags = (current_tags AND and_tags) XOR xor_tags
|
||||
func (i *ZdwlIpcOutputV2) SetClientTags(andTags, xorTags uint32) error {
|
||||
const opcode = 2
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(andTags))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(xorTags))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetLayout : Set the layout of this output
|
||||
//
|
||||
// index: index of a layout recieved by dwl_ipc_manager.layout
|
||||
func (i *ZdwlIpcOutputV2) SetLayout(index uint32) error {
|
||||
const opcode = 3
|
||||
const _reqBufLen = 8 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(index))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type ZdwlIpcOutputV2TagState uint32
|
||||
|
||||
// ZdwlIpcOutputV2TagState :
|
||||
const (
|
||||
// ZdwlIpcOutputV2TagStateNone : no state
|
||||
ZdwlIpcOutputV2TagStateNone ZdwlIpcOutputV2TagState = 0
|
||||
// ZdwlIpcOutputV2TagStateActive : tag is active
|
||||
ZdwlIpcOutputV2TagStateActive ZdwlIpcOutputV2TagState = 1
|
||||
// ZdwlIpcOutputV2TagStateUrgent : tag has at least one urgent client
|
||||
ZdwlIpcOutputV2TagStateUrgent ZdwlIpcOutputV2TagState = 2
|
||||
)
|
||||
|
||||
func (e ZdwlIpcOutputV2TagState) Name() string {
|
||||
switch e {
|
||||
case ZdwlIpcOutputV2TagStateNone:
|
||||
return "none"
|
||||
case ZdwlIpcOutputV2TagStateActive:
|
||||
return "active"
|
||||
case ZdwlIpcOutputV2TagStateUrgent:
|
||||
return "urgent"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZdwlIpcOutputV2TagState) Value() string {
|
||||
switch e {
|
||||
case ZdwlIpcOutputV2TagStateNone:
|
||||
return "0"
|
||||
case ZdwlIpcOutputV2TagStateActive:
|
||||
return "1"
|
||||
case ZdwlIpcOutputV2TagStateUrgent:
|
||||
return "2"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZdwlIpcOutputV2TagState) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2ToggleVisibilityEvent : Toggle client visibilty
|
||||
//
|
||||
// Indicates the client should hide or show themselves.
|
||||
// If the client is visible then hide, if hidden then show.
|
||||
type ZdwlIpcOutputV2ToggleVisibilityEvent struct{}
|
||||
type ZdwlIpcOutputV2ToggleVisibilityHandlerFunc func(ZdwlIpcOutputV2ToggleVisibilityEvent)
|
||||
|
||||
// SetToggleVisibilityHandler : sets handler for ZdwlIpcOutputV2ToggleVisibilityEvent
|
||||
func (i *ZdwlIpcOutputV2) SetToggleVisibilityHandler(f ZdwlIpcOutputV2ToggleVisibilityHandlerFunc) {
|
||||
i.toggleVisibilityHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2ActiveEvent : Update the selected output.
|
||||
//
|
||||
// Indicates if the output is active. Zero is invalid, nonzero is valid.
|
||||
type ZdwlIpcOutputV2ActiveEvent struct {
|
||||
Active uint32
|
||||
}
|
||||
type ZdwlIpcOutputV2ActiveHandlerFunc func(ZdwlIpcOutputV2ActiveEvent)
|
||||
|
||||
// SetActiveHandler : sets handler for ZdwlIpcOutputV2ActiveEvent
|
||||
func (i *ZdwlIpcOutputV2) SetActiveHandler(f ZdwlIpcOutputV2ActiveHandlerFunc) {
|
||||
i.activeHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2TagEvent : Update the state of a tag.
|
||||
//
|
||||
// Indicates that a tag has been updated.
|
||||
type ZdwlIpcOutputV2TagEvent struct {
|
||||
Tag uint32
|
||||
State uint32
|
||||
Clients uint32
|
||||
Focused uint32
|
||||
}
|
||||
type ZdwlIpcOutputV2TagHandlerFunc func(ZdwlIpcOutputV2TagEvent)
|
||||
|
||||
// SetTagHandler : sets handler for ZdwlIpcOutputV2TagEvent
|
||||
func (i *ZdwlIpcOutputV2) SetTagHandler(f ZdwlIpcOutputV2TagHandlerFunc) {
|
||||
i.tagHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2LayoutEvent : Update the layout.
|
||||
//
|
||||
// Indicates a new layout is selected.
|
||||
type ZdwlIpcOutputV2LayoutEvent struct {
|
||||
Layout uint32
|
||||
}
|
||||
type ZdwlIpcOutputV2LayoutHandlerFunc func(ZdwlIpcOutputV2LayoutEvent)
|
||||
|
||||
// SetLayoutHandler : sets handler for ZdwlIpcOutputV2LayoutEvent
|
||||
func (i *ZdwlIpcOutputV2) SetLayoutHandler(f ZdwlIpcOutputV2LayoutHandlerFunc) {
|
||||
i.layoutHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2TitleEvent : Update the title.
|
||||
//
|
||||
// Indicates the title has changed.
|
||||
type ZdwlIpcOutputV2TitleEvent struct {
|
||||
Title string
|
||||
}
|
||||
type ZdwlIpcOutputV2TitleHandlerFunc func(ZdwlIpcOutputV2TitleEvent)
|
||||
|
||||
// SetTitleHandler : sets handler for ZdwlIpcOutputV2TitleEvent
|
||||
func (i *ZdwlIpcOutputV2) SetTitleHandler(f ZdwlIpcOutputV2TitleHandlerFunc) {
|
||||
i.titleHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2AppidEvent : Update the appid.
|
||||
//
|
||||
// Indicates the appid has changed.
|
||||
type ZdwlIpcOutputV2AppidEvent struct {
|
||||
Appid string
|
||||
}
|
||||
type ZdwlIpcOutputV2AppidHandlerFunc func(ZdwlIpcOutputV2AppidEvent)
|
||||
|
||||
// SetAppidHandler : sets handler for ZdwlIpcOutputV2AppidEvent
|
||||
func (i *ZdwlIpcOutputV2) SetAppidHandler(f ZdwlIpcOutputV2AppidHandlerFunc) {
|
||||
i.appidHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2LayoutSymbolEvent : Update the current layout symbol
|
||||
//
|
||||
// Indicates the layout has changed. Since layout symbols are dynamic.
|
||||
// As opposed to the zdwl_ipc_manager.layout event, this should take precendence when displaying.
|
||||
// You can ignore the zdwl_ipc_output.layout event.
|
||||
type ZdwlIpcOutputV2LayoutSymbolEvent struct {
|
||||
Layout string
|
||||
}
|
||||
type ZdwlIpcOutputV2LayoutSymbolHandlerFunc func(ZdwlIpcOutputV2LayoutSymbolEvent)
|
||||
|
||||
// SetLayoutSymbolHandler : sets handler for ZdwlIpcOutputV2LayoutSymbolEvent
|
||||
func (i *ZdwlIpcOutputV2) SetLayoutSymbolHandler(f ZdwlIpcOutputV2LayoutSymbolHandlerFunc) {
|
||||
i.layoutSymbolHandler = f
|
||||
}
|
||||
|
||||
// ZdwlIpcOutputV2FrameEvent : The update sequence is done.
|
||||
//
|
||||
// Indicates that a sequence of status updates have finished and the client should redraw.
|
||||
type ZdwlIpcOutputV2FrameEvent struct{}
|
||||
type ZdwlIpcOutputV2FrameHandlerFunc func(ZdwlIpcOutputV2FrameEvent)
|
||||
|
||||
// SetFrameHandler : sets handler for ZdwlIpcOutputV2FrameEvent
|
||||
func (i *ZdwlIpcOutputV2) SetFrameHandler(f ZdwlIpcOutputV2FrameHandlerFunc) {
|
||||
i.frameHandler = f
|
||||
}
|
||||
|
||||
func (i *ZdwlIpcOutputV2) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if i.toggleVisibilityHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2ToggleVisibilityEvent
|
||||
|
||||
i.toggleVisibilityHandler(e)
|
||||
case 1:
|
||||
if i.activeHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2ActiveEvent
|
||||
l := 0
|
||||
e.Active = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.activeHandler(e)
|
||||
case 2:
|
||||
if i.tagHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2TagEvent
|
||||
l := 0
|
||||
e.Tag = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.State = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Clients = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
e.Focused = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.tagHandler(e)
|
||||
case 3:
|
||||
if i.layoutHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2LayoutEvent
|
||||
l := 0
|
||||
e.Layout = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.layoutHandler(e)
|
||||
case 4:
|
||||
if i.titleHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2TitleEvent
|
||||
l := 0
|
||||
titleLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
||||
l += 4
|
||||
e.Title = client.String(data[l : l+titleLen])
|
||||
l += titleLen
|
||||
|
||||
i.titleHandler(e)
|
||||
case 5:
|
||||
if i.appidHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2AppidEvent
|
||||
l := 0
|
||||
appidLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
||||
l += 4
|
||||
e.Appid = client.String(data[l : l+appidLen])
|
||||
l += appidLen
|
||||
|
||||
i.appidHandler(e)
|
||||
case 6:
|
||||
if i.layoutSymbolHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2LayoutSymbolEvent
|
||||
l := 0
|
||||
layoutLen := client.PaddedLen(int(client.Uint32(data[l : l+4])))
|
||||
l += 4
|
||||
e.Layout = client.String(data[l : l+layoutLen])
|
||||
l += layoutLen
|
||||
|
||||
i.layoutSymbolHandler(e)
|
||||
case 7:
|
||||
if i.frameHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZdwlIpcOutputV2FrameEvent
|
||||
|
||||
i.frameHandler(e)
|
||||
}
|
||||
}
|
||||
268
internal/proto/wlr_gamma_control/gamma_control.go
Normal file
268
internal/proto/wlr_gamma_control/gamma_control.go
Normal file
@@ -0,0 +1,268 @@
|
||||
// Generated by go-wayland-scanner
|
||||
// https://github.com/yaslama/go-wayland/cmd/go-wayland-scanner
|
||||
// XML file : wayland-protocols/wlr-gamma-control-unstable-v1.xml
|
||||
//
|
||||
// wlr_gamma_control_unstable_v1 Protocol Copyright:
|
||||
//
|
||||
// Copyright © 2015 Giulio camuffo
|
||||
// Copyright © 2018 Simon Ser
|
||||
//
|
||||
// Permission to use, copy, modify, distribute, and sell this
|
||||
// software and its documentation for any purpose is hereby granted
|
||||
// without fee, provided that the above copyright notice appear in
|
||||
// all copies and that both that copyright notice and this permission
|
||||
// notice appear in supporting documentation, and that the name of
|
||||
// the copyright holders not be used in advertising or publicity
|
||||
// pertaining to distribution of the software without specific,
|
||||
// written prior permission. The copyright holders make no
|
||||
// representations about the suitability of this software for any
|
||||
// purpose. It is provided "as is" without express or implied
|
||||
// warranty.
|
||||
//
|
||||
// THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
// SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
// FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
// ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
// THIS SOFTWARE.
|
||||
|
||||
package wlr_gamma_control
|
||||
|
||||
import (
|
||||
"github.com/yaslama/go-wayland/wayland/client"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ZwlrGammaControlManagerV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwlrGammaControlManagerV1InterfaceName = "zwlr_gamma_control_manager_v1"
|
||||
|
||||
// ZwlrGammaControlManagerV1 : manager to create per-output gamma controls
|
||||
//
|
||||
// This interface is a manager that allows creating per-output gamma
|
||||
// controls.
|
||||
type ZwlrGammaControlManagerV1 struct {
|
||||
client.BaseProxy
|
||||
}
|
||||
|
||||
// NewZwlrGammaControlManagerV1 : manager to create per-output gamma controls
|
||||
//
|
||||
// This interface is a manager that allows creating per-output gamma
|
||||
// controls.
|
||||
func NewZwlrGammaControlManagerV1(ctx *client.Context) *ZwlrGammaControlManagerV1 {
|
||||
zwlrGammaControlManagerV1 := &ZwlrGammaControlManagerV1{}
|
||||
ctx.Register(zwlrGammaControlManagerV1)
|
||||
return zwlrGammaControlManagerV1
|
||||
}
|
||||
|
||||
// GetGammaControl : get a gamma control for an output
|
||||
//
|
||||
// Create a gamma control that can be used to adjust gamma tables for the
|
||||
// provided output.
|
||||
func (i *ZwlrGammaControlManagerV1) GetGammaControl(output *client.Output) (*ZwlrGammaControlV1, error) {
|
||||
id := NewZwlrGammaControlV1(i.Context())
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8 + 4 + 4
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], id.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], output.ID())
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// Destroy : destroy the manager
|
||||
//
|
||||
// All objects created by the manager will still remain valid, until their
|
||||
// appropriate destroy request has been called.
|
||||
func (i *ZwlrGammaControlManagerV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// ZwlrGammaControlV1InterfaceName is the name of the interface as it appears in the [client.Registry].
|
||||
// It can be used to match the [client.RegistryGlobalEvent.Interface] in the
|
||||
// [Registry.SetGlobalHandler] and can be used in [Registry.Bind] if this applies.
|
||||
const ZwlrGammaControlV1InterfaceName = "zwlr_gamma_control_v1"
|
||||
|
||||
// ZwlrGammaControlV1 : adjust gamma tables for an output
|
||||
//
|
||||
// This interface allows a client to adjust gamma tables for a particular
|
||||
// output.
|
||||
//
|
||||
// The client will receive the gamma size, and will then be able to set gamma
|
||||
// tables. At any time the compositor can send a failed event indicating that
|
||||
// this object is no longer valid.
|
||||
//
|
||||
// There can only be at most one gamma control object per output, which
|
||||
// has exclusive access to this particular output. When the gamma control
|
||||
// object is destroyed, the gamma table is restored to its original value.
|
||||
type ZwlrGammaControlV1 struct {
|
||||
client.BaseProxy
|
||||
gammaSizeHandler ZwlrGammaControlV1GammaSizeHandlerFunc
|
||||
failedHandler ZwlrGammaControlV1FailedHandlerFunc
|
||||
}
|
||||
|
||||
// NewZwlrGammaControlV1 : adjust gamma tables for an output
|
||||
//
|
||||
// This interface allows a client to adjust gamma tables for a particular
|
||||
// output.
|
||||
//
|
||||
// The client will receive the gamma size, and will then be able to set gamma
|
||||
// tables. At any time the compositor can send a failed event indicating that
|
||||
// this object is no longer valid.
|
||||
//
|
||||
// There can only be at most one gamma control object per output, which
|
||||
// has exclusive access to this particular output. When the gamma control
|
||||
// object is destroyed, the gamma table is restored to its original value.
|
||||
func NewZwlrGammaControlV1(ctx *client.Context) *ZwlrGammaControlV1 {
|
||||
zwlrGammaControlV1 := &ZwlrGammaControlV1{}
|
||||
ctx.Register(zwlrGammaControlV1)
|
||||
return zwlrGammaControlV1
|
||||
}
|
||||
|
||||
// SetGamma : set the gamma table
|
||||
//
|
||||
// Set the gamma table. The file descriptor can be memory-mapped to provide
|
||||
// the raw gamma table, which contains successive gamma ramps for the red,
|
||||
// green and blue channels. Each gamma ramp is an array of 16-byte unsigned
|
||||
// integers which has the same length as the gamma size.
|
||||
//
|
||||
// The file descriptor data must have the same length as three times the
|
||||
// gamma size.
|
||||
//
|
||||
// fd: gamma table file descriptor
|
||||
func (i *ZwlrGammaControlV1) SetGamma(fd int) error {
|
||||
const opcode = 0
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
oob := unix.UnixRights(int(fd))
|
||||
err := i.Context().WriteMsg(_reqBuf[:], oob)
|
||||
return err
|
||||
}
|
||||
|
||||
// Destroy : destroy this control
|
||||
//
|
||||
// Destroys the gamma control object. If the object is still valid, this
|
||||
// restores the original gamma tables.
|
||||
func (i *ZwlrGammaControlV1) Destroy() error {
|
||||
defer i.Context().Unregister(i)
|
||||
const opcode = 1
|
||||
const _reqBufLen = 8
|
||||
var _reqBuf [_reqBufLen]byte
|
||||
l := 0
|
||||
client.PutUint32(_reqBuf[l:4], i.ID())
|
||||
l += 4
|
||||
client.PutUint32(_reqBuf[l:l+4], uint32(_reqBufLen<<16|opcode&0x0000ffff))
|
||||
l += 4
|
||||
err := i.Context().WriteMsg(_reqBuf[:], nil)
|
||||
return err
|
||||
}
|
||||
|
||||
type ZwlrGammaControlV1Error uint32
|
||||
|
||||
// ZwlrGammaControlV1Error :
|
||||
const (
|
||||
// ZwlrGammaControlV1ErrorInvalidGamma : invalid gamma tables
|
||||
ZwlrGammaControlV1ErrorInvalidGamma ZwlrGammaControlV1Error = 1
|
||||
)
|
||||
|
||||
func (e ZwlrGammaControlV1Error) Name() string {
|
||||
switch e {
|
||||
case ZwlrGammaControlV1ErrorInvalidGamma:
|
||||
return "invalid_gamma"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrGammaControlV1Error) Value() string {
|
||||
switch e {
|
||||
case ZwlrGammaControlV1ErrorInvalidGamma:
|
||||
return "1"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (e ZwlrGammaControlV1Error) String() string {
|
||||
return e.Name() + "=" + e.Value()
|
||||
}
|
||||
|
||||
// ZwlrGammaControlV1GammaSizeEvent : size of gamma ramps
|
||||
//
|
||||
// Advertise the size of each gamma ramp.
|
||||
//
|
||||
// This event is sent immediately when the gamma control object is created.
|
||||
type ZwlrGammaControlV1GammaSizeEvent struct {
|
||||
Size uint32
|
||||
}
|
||||
type ZwlrGammaControlV1GammaSizeHandlerFunc func(ZwlrGammaControlV1GammaSizeEvent)
|
||||
|
||||
// SetGammaSizeHandler : sets handler for ZwlrGammaControlV1GammaSizeEvent
|
||||
func (i *ZwlrGammaControlV1) SetGammaSizeHandler(f ZwlrGammaControlV1GammaSizeHandlerFunc) {
|
||||
i.gammaSizeHandler = f
|
||||
}
|
||||
|
||||
// ZwlrGammaControlV1FailedEvent : object no longer valid
|
||||
//
|
||||
// This event indicates that the gamma control is no longer valid. This
|
||||
// can happen for a number of reasons, including:
|
||||
// - The output doesn't support gamma tables
|
||||
// - Setting the gamma tables failed
|
||||
// - Another client already has exclusive gamma control for this output
|
||||
// - The compositor has transferred gamma control to another client
|
||||
//
|
||||
// Upon receiving this event, the client should destroy this object.
|
||||
type ZwlrGammaControlV1FailedEvent struct{}
|
||||
type ZwlrGammaControlV1FailedHandlerFunc func(ZwlrGammaControlV1FailedEvent)
|
||||
|
||||
// SetFailedHandler : sets handler for ZwlrGammaControlV1FailedEvent
|
||||
func (i *ZwlrGammaControlV1) SetFailedHandler(f ZwlrGammaControlV1FailedHandlerFunc) {
|
||||
i.failedHandler = f
|
||||
}
|
||||
|
||||
func (i *ZwlrGammaControlV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
switch opcode {
|
||||
case 0:
|
||||
if i.gammaSizeHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrGammaControlV1GammaSizeEvent
|
||||
l := 0
|
||||
e.Size = client.Uint32(data[l : l+4])
|
||||
l += 4
|
||||
|
||||
i.gammaSizeHandler(e)
|
||||
case 1:
|
||||
if i.failedHandler == nil {
|
||||
return
|
||||
}
|
||||
var e ZwlrGammaControlV1FailedEvent
|
||||
|
||||
i.failedHandler(e)
|
||||
}
|
||||
}
|
||||
166
internal/proto/xml/dwl-ipc-unstable-v2.xml
Normal file
166
internal/proto/xml/dwl-ipc-unstable-v2.xml
Normal file
@@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
This is largely ripped from somebar's ipc patchset; just with some personal modifications.
|
||||
I would probably just submit raphi's patchset but I don't think that would be polite.
|
||||
-->
|
||||
<protocol name="dwl_ipc_unstable_v2">
|
||||
<description summary="inter-proccess-communication about dwl's state">
|
||||
This protocol allows clients to update and get updates from dwl.
|
||||
|
||||
Warning! The protocol described in this file is experimental and
|
||||
backward incompatible changes may be made. Backward compatible
|
||||
changes may be added together with the corresponding interface
|
||||
version bump.
|
||||
Backward incompatible changes are done by bumping the version
|
||||
number in the protocol and interface names and resetting the
|
||||
interface version. Once the protocol is to be declared stable,
|
||||
the 'z' prefix and the version number in the protocol and
|
||||
interface names are removed and the interface version number is
|
||||
reset.
|
||||
</description>
|
||||
|
||||
<interface name="zdwl_ipc_manager_v2" version="1">
|
||||
<description summary="manage dwl state">
|
||||
This interface is exposed as a global in wl_registry.
|
||||
|
||||
Clients can use this interface to get a dwl_ipc_output.
|
||||
After binding the client will recieve the dwl_ipc_manager.tags and dwl_ipc_manager.layout events.
|
||||
The dwl_ipc_manager.tags and dwl_ipc_manager.layout events expose tags and layouts to the client.
|
||||
</description>
|
||||
|
||||
<request name="release" type="destructor">
|
||||
<description summary="release dwl_ipc_manager">
|
||||
Indicates that the client will not the dwl_ipc_manager object anymore.
|
||||
Objects created through this instance are not affected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_output">
|
||||
<description summary="get a dwl_ipc_outout for a wl_output">
|
||||
Get a dwl_ipc_outout for the specified wl_output.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zdwl_ipc_output_v2"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
|
||||
<event name="tags">
|
||||
<description summary="Announces tag amount">
|
||||
This event is sent after binding.
|
||||
A roundtrip after binding guarantees the client recieved all tags.
|
||||
</description>
|
||||
<arg name="amount" type="uint"/>
|
||||
</event>
|
||||
|
||||
<event name="layout">
|
||||
<description summary="Announces a layout">
|
||||
This event is sent after binding.
|
||||
A roundtrip after binding guarantees the client recieved all layouts.
|
||||
</description>
|
||||
<arg name="name" type="string"/>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="zdwl_ipc_output_v2" version="1">
|
||||
<description summary="control dwl output">
|
||||
Observe and control a dwl output.
|
||||
|
||||
Events are double-buffered:
|
||||
Clients should cache events and redraw when a dwl_ipc_output.frame event is sent.
|
||||
|
||||
Request are not double-buffered:
|
||||
The compositor will update immediately upon request.
|
||||
</description>
|
||||
|
||||
<enum name="tag_state">
|
||||
<entry name="none" value="0" summary="no state"/>
|
||||
<entry name="active" value="1" summary="tag is active"/>
|
||||
<entry name="urgent" value="2" summary="tag has at least one urgent client"/>
|
||||
</enum>
|
||||
|
||||
<request name="release" type="destructor">
|
||||
<description summary="release dwl_ipc_outout">
|
||||
Indicates to that the client no longer needs this dwl_ipc_output.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="toggle_visibility">
|
||||
<description summary="Toggle client visibilty">
|
||||
Indicates the client should hide or show themselves.
|
||||
If the client is visible then hide, if hidden then show.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="active">
|
||||
<description summary="Update the selected output.">
|
||||
Indicates if the output is active. Zero is invalid, nonzero is valid.
|
||||
</description>
|
||||
<arg name="active" type="uint"/>
|
||||
</event>
|
||||
|
||||
<event name="tag">
|
||||
<description summary="Update the state of a tag.">
|
||||
Indicates that a tag has been updated.
|
||||
</description>
|
||||
<arg name="tag" type="uint" summary="Index of the tag"/>
|
||||
<arg name="state" type="uint" enum="tag_state" summary="The state of the tag."/>
|
||||
<arg name="clients" type="uint" summary="The number of clients in the tag."/>
|
||||
<arg name="focused" type="uint" summary="If there is a focused client. Nonzero being valid, zero being invalid."/>
|
||||
</event>
|
||||
|
||||
<event name="layout">
|
||||
<description summary="Update the layout.">
|
||||
Indicates a new layout is selected.
|
||||
</description>
|
||||
<arg name="layout" type="uint" summary="Index of the layout."/>
|
||||
</event>
|
||||
|
||||
<event name="title">
|
||||
<description summary="Update the title.">
|
||||
Indicates the title has changed.
|
||||
</description>
|
||||
<arg name="title" type="string" summary="The new title name."/>
|
||||
</event>
|
||||
|
||||
<event name="appid" since="1">
|
||||
<description summary="Update the appid.">
|
||||
Indicates the appid has changed.
|
||||
</description>
|
||||
<arg name="appid" type="string" summary="The new appid."/>
|
||||
</event>
|
||||
|
||||
<event name="layout_symbol" since="1">
|
||||
<description summary="Update the current layout symbol">
|
||||
Indicates the layout has changed. Since layout symbols are dynamic.
|
||||
As opposed to the zdwl_ipc_manager.layout event, this should take precendence when displaying.
|
||||
You can ignore the zdwl_ipc_output.layout event.
|
||||
</description>
|
||||
<arg name="layout" type="string" summary="The new layout"/>
|
||||
</event>
|
||||
|
||||
<event name="frame">
|
||||
<description summary="The update sequence is done.">
|
||||
Indicates that a sequence of status updates have finished and the client should redraw.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="set_tags">
|
||||
<description summary="Set the active tags of this output"/>
|
||||
<arg name="tagmask" type="uint" summary="bitmask of the tags that should be set."/>
|
||||
<arg name="toggle_tagset" type="uint" summary="toggle the selected tagset, zero for invalid, nonzero for valid."/>
|
||||
</request>
|
||||
|
||||
<request name="set_client_tags">
|
||||
<description summary="Set the tags of the focused client.">
|
||||
The tags are updated as follows:
|
||||
new_tags = (current_tags AND and_tags) XOR xor_tags
|
||||
</description>
|
||||
<arg name="and_tags" type="uint"/>
|
||||
<arg name="xor_tags" type="uint"/>
|
||||
</request>
|
||||
|
||||
<request name="set_layout">
|
||||
<description summary="Set the layout of this output"/>
|
||||
<arg name="index" type="uint" summary="index of a layout recieved by dwl_ipc_manager.layout"/>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
126
internal/proto/xml/wlr-gamma-control-unstable-v1.xml
Normal file
126
internal/proto/xml/wlr-gamma-control-unstable-v1.xml
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_gamma_control_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2015 Giulio camuffo
|
||||
Copyright © 2018 Simon Ser
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this
|
||||
software and its documentation for any purpose is hereby granted
|
||||
without fee, provided that the above copyright notice appear in
|
||||
all copies and that both that copyright notice and this permission
|
||||
notice appear in supporting documentation, and that the name of
|
||||
the copyright holders not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific,
|
||||
written prior permission. The copyright holders make no
|
||||
representations about the suitability of this software for any
|
||||
purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
||||
|
||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="manage gamma tables of outputs">
|
||||
This protocol allows a privileged client to set the gamma tables for
|
||||
outputs.
|
||||
|
||||
Warning! The protocol described in this file is experimental and
|
||||
backward incompatible changes may be made. Backward compatible changes
|
||||
may be added together with the corresponding interface version bump.
|
||||
Backward incompatible changes are done by bumping the version number in
|
||||
the protocol and interface names and resetting the interface version.
|
||||
Once the protocol is to be declared stable, the 'z' prefix and the
|
||||
version number in the protocol and interface names are removed and the
|
||||
interface version number is reset.
|
||||
</description>
|
||||
|
||||
<interface name="zwlr_gamma_control_manager_v1" version="1">
|
||||
<description summary="manager to create per-output gamma controls">
|
||||
This interface is a manager that allows creating per-output gamma
|
||||
controls.
|
||||
</description>
|
||||
|
||||
<request name="get_gamma_control">
|
||||
<description summary="get a gamma control for an output">
|
||||
Create a gamma control that can be used to adjust gamma tables for the
|
||||
provided output.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zwlr_gamma_control_v1"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the manager">
|
||||
All objects created by the manager will still remain valid, until their
|
||||
appropriate destroy request has been called.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_gamma_control_v1" version="1">
|
||||
<description summary="adjust gamma tables for an output">
|
||||
This interface allows a client to adjust gamma tables for a particular
|
||||
output.
|
||||
|
||||
The client will receive the gamma size, and will then be able to set gamma
|
||||
tables. At any time the compositor can send a failed event indicating that
|
||||
this object is no longer valid.
|
||||
|
||||
There can only be at most one gamma control object per output, which
|
||||
has exclusive access to this particular output. When the gamma control
|
||||
object is destroyed, the gamma table is restored to its original value.
|
||||
</description>
|
||||
|
||||
<event name="gamma_size">
|
||||
<description summary="size of gamma ramps">
|
||||
Advertise the size of each gamma ramp.
|
||||
|
||||
This event is sent immediately when the gamma control object is created.
|
||||
</description>
|
||||
<arg name="size" type="uint" summary="number of elements in a ramp"/>
|
||||
</event>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_gamma" value="1" summary="invalid gamma tables"/>
|
||||
</enum>
|
||||
|
||||
<request name="set_gamma">
|
||||
<description summary="set the gamma table">
|
||||
Set the gamma table. The file descriptor can be memory-mapped to provide
|
||||
the raw gamma table, which contains successive gamma ramps for the red,
|
||||
green and blue channels. Each gamma ramp is an array of 16-byte unsigned
|
||||
integers which has the same length as the gamma size.
|
||||
|
||||
The file descriptor data must have the same length as three times the
|
||||
gamma size.
|
||||
</description>
|
||||
<arg name="fd" type="fd" summary="gamma table file descriptor"/>
|
||||
</request>
|
||||
|
||||
<event name="failed">
|
||||
<description summary="object no longer valid">
|
||||
This event indicates that the gamma control is no longer valid. This
|
||||
can happen for a number of reasons, including:
|
||||
- The output doesn't support gamma tables
|
||||
- Setting the gamma tables failed
|
||||
- Another client already has exclusive gamma control for this output
|
||||
- The compositor has transferred gamma control to another client
|
||||
|
||||
Upon receiving this event, the client should destroy this object.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy this control">
|
||||
Destroys the gamma control object. If the object is still valid, this
|
||||
restores the original gamma tables.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
||||
341
internal/server/bluez/agent.go
Normal file
341
internal/server/bluez/agent.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/errdefs"
|
||||
"github.com/AvengeMedia/danklinux/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
bluezService = "org.bluez"
|
||||
agentManagerPath = "/org/bluez"
|
||||
agentManagerIface = "org.bluez.AgentManager1"
|
||||
agent1Iface = "org.bluez.Agent1"
|
||||
device1Iface = "org.bluez.Device1"
|
||||
agentPath = "/com/danklinux/bluez/agent"
|
||||
agentCapability = "KeyboardDisplay"
|
||||
)
|
||||
|
||||
const introspectXML = `
|
||||
<node>
|
||||
<interface name="org.bluez.Agent1">
|
||||
<method name="Release"/>
|
||||
<method name="RequestPinCode">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
<arg direction="out" type="s" name="pincode"/>
|
||||
</method>
|
||||
<method name="RequestPasskey">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
<arg direction="out" type="u" name="passkey"/>
|
||||
</method>
|
||||
<method name="DisplayPinCode">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
<arg direction="in" type="s" name="pincode"/>
|
||||
</method>
|
||||
<method name="DisplayPasskey">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
<arg direction="in" type="u" name="passkey"/>
|
||||
<arg direction="in" type="q" name="entered"/>
|
||||
</method>
|
||||
<method name="RequestConfirmation">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
<arg direction="in" type="u" name="passkey"/>
|
||||
</method>
|
||||
<method name="RequestAuthorization">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
</method>
|
||||
<method name="AuthorizeService">
|
||||
<arg direction="in" type="o" name="device"/>
|
||||
<arg direction="in" type="s" name="uuid"/>
|
||||
</method>
|
||||
<method name="Cancel"/>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Introspectable">
|
||||
<method name="Introspect">
|
||||
<arg direction="out" type="s" name="data"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>`
|
||||
|
||||
type BluezAgent struct {
|
||||
conn *dbus.Conn
|
||||
broker PromptBroker
|
||||
}
|
||||
|
||||
func NewBluezAgent(broker PromptBroker) (*BluezAgent, error) {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
||||
}
|
||||
|
||||
agent := &BluezAgent{
|
||||
conn: conn,
|
||||
broker: broker,
|
||||
}
|
||||
|
||||
if err := conn.Export(agent, dbus.ObjectPath(agentPath), agent1Iface); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("agent export failed: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.Export(agent, dbus.ObjectPath(agentPath), "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("introspection export failed: %w", err)
|
||||
}
|
||||
|
||||
mgr := conn.Object(bluezService, dbus.ObjectPath(agentManagerPath))
|
||||
if err := mgr.Call(agentManagerIface+".RegisterAgent", 0, dbus.ObjectPath(agentPath), agentCapability).Err; err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("agent registration failed: %w", err)
|
||||
}
|
||||
|
||||
if err := mgr.Call(agentManagerIface+".RequestDefaultAgent", 0, dbus.ObjectPath(agentPath)).Err; err != nil {
|
||||
log.Debugf("[BluezAgent] not default agent: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("[BluezAgent] registered at %s with capability %s", agentPath, agentCapability)
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) Close() {
|
||||
if a.conn == nil {
|
||||
return
|
||||
}
|
||||
mgr := a.conn.Object(bluezService, dbus.ObjectPath(agentManagerPath))
|
||||
mgr.Call(agentManagerIface+".UnregisterAgent", 0, dbus.ObjectPath(agentPath))
|
||||
a.conn.Close()
|
||||
}
|
||||
|
||||
func (a *BluezAgent) Release() *dbus.Error {
|
||||
log.Infof("[BluezAgent] Release called")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) RequestPinCode(device dbus.ObjectPath) (string, *dbus.Error) {
|
||||
log.Infof("[BluezAgent] RequestPinCode: device=%s", device)
|
||||
|
||||
secrets, err := a.promptFor(device, "pin", []string{"pin"}, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] RequestPinCode failed: %v", err)
|
||||
return "", a.errorFrom(err)
|
||||
}
|
||||
|
||||
pin := secrets["pin"]
|
||||
log.Infof("[BluezAgent] RequestPinCode returning PIN (len=%d)", len(pin))
|
||||
return pin, nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) {
|
||||
log.Infof("[BluezAgent] RequestPasskey: device=%s", device)
|
||||
|
||||
secrets, err := a.promptFor(device, "passkey", []string{"passkey"}, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] RequestPasskey failed: %v", err)
|
||||
return 0, a.errorFrom(err)
|
||||
}
|
||||
|
||||
passkey, err := strconv.ParseUint(secrets["passkey"], 10, 32)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] invalid passkey format: %v", err)
|
||||
return 0, dbus.MakeFailedError(fmt.Errorf("invalid passkey: %w", err))
|
||||
}
|
||||
|
||||
log.Infof("[BluezAgent] RequestPasskey returning: %d", passkey)
|
||||
return uint32(passkey), nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error {
|
||||
log.Infof("[BluezAgent] DisplayPinCode: device=%s, pin=%s", device, pincode)
|
||||
|
||||
_, err := a.promptFor(device, "display-pin", []string{}, &pincode)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] DisplayPinCode acknowledgment failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error {
|
||||
log.Infof("[BluezAgent] DisplayPasskey: device=%s, passkey=%06d, entered=%d", device, passkey, entered)
|
||||
|
||||
if entered == 0 {
|
||||
pk := passkey
|
||||
_, err := a.promptFor(device, "display-passkey", []string{}, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] DisplayPasskey acknowledgment failed: %v", err)
|
||||
}
|
||||
_ = pk
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error {
|
||||
log.Infof("[BluezAgent] RequestConfirmation: device=%s, passkey=%06d", device, passkey)
|
||||
|
||||
secrets, err := a.promptFor(device, "confirm", []string{"decision"}, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] RequestConfirmation failed: %v", err)
|
||||
return a.errorFrom(err)
|
||||
}
|
||||
|
||||
if secrets["decision"] != "yes" && secrets["decision"] != "accept" {
|
||||
log.Debugf("[BluezAgent] RequestConfirmation rejected by user")
|
||||
return dbus.NewError("org.bluez.Error.Rejected", nil)
|
||||
}
|
||||
|
||||
log.Infof("[BluezAgent] RequestConfirmation accepted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error {
|
||||
log.Infof("[BluezAgent] RequestAuthorization: device=%s", device)
|
||||
|
||||
secrets, err := a.promptFor(device, "authorize", []string{"decision"}, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] RequestAuthorization failed: %v", err)
|
||||
return a.errorFrom(err)
|
||||
}
|
||||
|
||||
if secrets["decision"] != "yes" && secrets["decision"] != "accept" {
|
||||
log.Debugf("[BluezAgent] RequestAuthorization rejected by user")
|
||||
return dbus.NewError("org.bluez.Error.Rejected", nil)
|
||||
}
|
||||
|
||||
log.Infof("[BluezAgent] RequestAuthorization accepted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error {
|
||||
log.Infof("[BluezAgent] AuthorizeService: device=%s, uuid=%s", device, uuid)
|
||||
|
||||
secrets, err := a.promptFor(device, "authorize-service:"+uuid, []string{"decision"}, nil)
|
||||
if err != nil {
|
||||
log.Warnf("[BluezAgent] AuthorizeService failed: %v", err)
|
||||
return a.errorFrom(err)
|
||||
}
|
||||
|
||||
if secrets["decision"] != "yes" && secrets["decision"] != "accept" {
|
||||
log.Debugf("[BluezAgent] AuthorizeService rejected by user")
|
||||
return dbus.NewError("org.bluez.Error.Rejected", nil)
|
||||
}
|
||||
|
||||
log.Infof("[BluezAgent] AuthorizeService accepted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) Cancel() *dbus.Error {
|
||||
log.Infof("[BluezAgent] Cancel called")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) Introspect() (string, *dbus.Error) {
|
||||
return introspectXML, nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) promptFor(device dbus.ObjectPath, requestType string, fields []string, displayValue *string) (map[string]string, error) {
|
||||
if a.broker == nil {
|
||||
return nil, fmt.Errorf("broker not initialized")
|
||||
}
|
||||
|
||||
deviceName, deviceAddr := a.getDeviceInfo(device)
|
||||
hints := []string{}
|
||||
if displayValue != nil {
|
||||
hints = append(hints, *displayValue)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var passkey *uint32
|
||||
if requestType == "confirm" || requestType == "display-passkey" {
|
||||
if displayValue != nil {
|
||||
if pk, err := strconv.ParseUint(*displayValue, 10, 32); err == nil {
|
||||
pk32 := uint32(pk)
|
||||
passkey = &pk32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
token, err := a.broker.Ask(ctx, PromptRequest{
|
||||
DevicePath: string(device),
|
||||
DeviceName: deviceName,
|
||||
DeviceAddr: deviceAddr,
|
||||
RequestType: requestType,
|
||||
Fields: fields,
|
||||
Hints: hints,
|
||||
Passkey: passkey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prompt creation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("[BluezAgent] waiting for user response (token=%s)", token)
|
||||
reply, err := a.broker.Wait(ctx, token)
|
||||
if err != nil {
|
||||
if errors.Is(err, errdefs.ErrSecretPromptTimeout) {
|
||||
return nil, err
|
||||
}
|
||||
if reply.Cancel || errors.Is(err, errdefs.ErrSecretPromptCancelled) {
|
||||
return nil, errdefs.ErrSecretPromptCancelled
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !reply.Accept && len(fields) > 0 {
|
||||
return nil, errdefs.ErrSecretPromptCancelled
|
||||
}
|
||||
|
||||
return reply.Secrets, nil
|
||||
}
|
||||
|
||||
func (a *BluezAgent) getDeviceInfo(device dbus.ObjectPath) (string, string) {
|
||||
obj := a.conn.Object(bluezService, device)
|
||||
|
||||
var name, alias, addr string
|
||||
|
||||
nameVar, err := obj.GetProperty(device1Iface + ".Name")
|
||||
if err == nil {
|
||||
if n, ok := nameVar.Value().(string); ok {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
|
||||
aliasVar, err := obj.GetProperty(device1Iface + ".Alias")
|
||||
if err == nil {
|
||||
if a, ok := aliasVar.Value().(string); ok {
|
||||
alias = a
|
||||
}
|
||||
}
|
||||
|
||||
addrVar, err := obj.GetProperty(device1Iface + ".Address")
|
||||
if err == nil {
|
||||
if a, ok := addrVar.Value().(string); ok {
|
||||
addr = a
|
||||
}
|
||||
}
|
||||
|
||||
if alias != "" {
|
||||
return alias, addr
|
||||
}
|
||||
if name != "" {
|
||||
return name, addr
|
||||
}
|
||||
return addr, addr
|
||||
}
|
||||
|
||||
func (a *BluezAgent) errorFrom(err error) *dbus.Error {
|
||||
if errors.Is(err, errdefs.ErrSecretPromptTimeout) {
|
||||
return dbus.NewError("org.bluez.Error.Canceled", nil)
|
||||
}
|
||||
if errors.Is(err, errdefs.ErrSecretPromptCancelled) {
|
||||
return dbus.NewError("org.bluez.Error.Canceled", nil)
|
||||
}
|
||||
return dbus.MakeFailedError(err)
|
||||
}
|
||||
21
internal/server/bluez/broker.go
Normal file
21
internal/server/bluez/broker.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
type PromptBroker interface {
|
||||
Ask(ctx context.Context, req PromptRequest) (token string, err error)
|
||||
Wait(ctx context.Context, token string) (PromptReply, error)
|
||||
Resolve(token string, reply PromptReply) error
|
||||
}
|
||||
|
||||
func generateToken() (string, error) {
|
||||
bytes := make([]byte, 16)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(bytes), nil
|
||||
}
|
||||
220
internal/server/bluez/broker_test.go
Normal file
220
internal/server/bluez/broker_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSubscriptionBrokerAskWait(t *testing.T) {
|
||||
promptReceived := false
|
||||
broker := NewSubscriptionBroker(func(p PairingPrompt) {
|
||||
promptReceived = true
|
||||
if p.Token == "" {
|
||||
t.Error("expected token to be non-empty")
|
||||
}
|
||||
if p.DeviceName != "TestDevice" {
|
||||
t.Errorf("expected DeviceName=TestDevice, got %s", p.DeviceName)
|
||||
}
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
req := PromptRequest{
|
||||
DevicePath: "/org/bluez/test",
|
||||
DeviceName: "TestDevice",
|
||||
DeviceAddr: "AA:BB:CC:DD:EE:FF",
|
||||
RequestType: "pin",
|
||||
Fields: []string{"pin"},
|
||||
}
|
||||
|
||||
token, err := broker.Ask(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask failed: %v", err)
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
t.Fatal("expected non-empty token")
|
||||
}
|
||||
|
||||
if !promptReceived {
|
||||
t.Fatal("expected prompt broadcast to be called")
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
broker.Resolve(token, PromptReply{
|
||||
Secrets: map[string]string{"pin": "1234"},
|
||||
Accept: true,
|
||||
})
|
||||
}()
|
||||
|
||||
reply, err := broker.Wait(ctx, token)
|
||||
if err != nil {
|
||||
t.Fatalf("Wait failed: %v", err)
|
||||
}
|
||||
|
||||
if reply.Secrets["pin"] != "1234" {
|
||||
t.Errorf("expected pin=1234, got %s", reply.Secrets["pin"])
|
||||
}
|
||||
|
||||
if !reply.Accept {
|
||||
t.Error("expected Accept=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionBrokerTimeout(t *testing.T) {
|
||||
broker := NewSubscriptionBroker(nil)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
req := PromptRequest{
|
||||
DevicePath: "/org/bluez/test",
|
||||
DeviceName: "TestDevice",
|
||||
RequestType: "passkey",
|
||||
Fields: []string{"passkey"},
|
||||
}
|
||||
|
||||
token, err := broker.Ask(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = broker.Wait(ctx, token)
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionBrokerCancel(t *testing.T) {
|
||||
broker := NewSubscriptionBroker(nil)
|
||||
|
||||
ctx := context.Background()
|
||||
req := PromptRequest{
|
||||
DevicePath: "/org/bluez/test",
|
||||
DeviceName: "TestDevice",
|
||||
RequestType: "confirm",
|
||||
Fields: []string{"decision"},
|
||||
}
|
||||
|
||||
token, err := broker.Ask(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask failed: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
broker.Resolve(token, PromptReply{
|
||||
Cancel: true,
|
||||
})
|
||||
}()
|
||||
|
||||
_, err = broker.Wait(ctx, token)
|
||||
if err == nil {
|
||||
t.Fatal("expected cancelled error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionBrokerUnknownToken(t *testing.T) {
|
||||
broker := NewSubscriptionBroker(nil)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := broker.Wait(ctx, "invalid-token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateToken(t *testing.T) {
|
||||
token1, err := generateToken()
|
||||
if err != nil {
|
||||
t.Fatalf("generateToken failed: %v", err)
|
||||
}
|
||||
|
||||
token2, err := generateToken()
|
||||
if err != nil {
|
||||
t.Fatalf("generateToken failed: %v", err)
|
||||
}
|
||||
|
||||
if token1 == token2 {
|
||||
t.Error("expected unique tokens")
|
||||
}
|
||||
|
||||
if len(token1) != 32 {
|
||||
t.Errorf("expected token length 32, got %d", len(token1))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionBrokerResolveUnknownToken(t *testing.T) {
|
||||
broker := NewSubscriptionBroker(nil)
|
||||
|
||||
err := broker.Resolve("unknown-token", PromptReply{
|
||||
Secrets: map[string]string{"test": "value"},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionBrokerMultipleRequests(t *testing.T) {
|
||||
broker := NewSubscriptionBroker(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req1 := PromptRequest{
|
||||
DevicePath: "/org/bluez/test1",
|
||||
DeviceName: "Device1",
|
||||
RequestType: "pin",
|
||||
Fields: []string{"pin"},
|
||||
}
|
||||
|
||||
req2 := PromptRequest{
|
||||
DevicePath: "/org/bluez/test2",
|
||||
DeviceName: "Device2",
|
||||
RequestType: "passkey",
|
||||
Fields: []string{"passkey"},
|
||||
}
|
||||
|
||||
token1, err := broker.Ask(ctx, req1)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask1 failed: %v", err)
|
||||
}
|
||||
|
||||
token2, err := broker.Ask(ctx, req2)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask2 failed: %v", err)
|
||||
}
|
||||
|
||||
if token1 == token2 {
|
||||
t.Error("expected different tokens")
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
broker.Resolve(token1, PromptReply{
|
||||
Secrets: map[string]string{"pin": "1234"},
|
||||
Accept: true,
|
||||
})
|
||||
broker.Resolve(token2, PromptReply{
|
||||
Secrets: map[string]string{"passkey": "567890"},
|
||||
Accept: true,
|
||||
})
|
||||
}()
|
||||
|
||||
reply1, err := broker.Wait(ctx, token1)
|
||||
if err != nil {
|
||||
t.Fatalf("Wait1 failed: %v", err)
|
||||
}
|
||||
|
||||
reply2, err := broker.Wait(ctx, token2)
|
||||
if err != nil {
|
||||
t.Fatalf("Wait2 failed: %v", err)
|
||||
}
|
||||
|
||||
if reply1.Secrets["pin"] != "1234" {
|
||||
t.Errorf("expected pin=1234, got %s", reply1.Secrets["pin"])
|
||||
}
|
||||
|
||||
if reply2.Secrets["passkey"] != "567890" {
|
||||
t.Errorf("expected passkey=567890, got %s", reply2.Secrets["passkey"])
|
||||
}
|
||||
}
|
||||
260
internal/server/bluez/handlers.go
Normal file
260
internal/server/bluez/handlers.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/server/models"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type SuccessResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type BluetoothEvent struct {
|
||||
Type string `json:"type"`
|
||||
Data BluetoothState `json:"data"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "bluetooth.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "bluetooth.startDiscovery":
|
||||
handleStartDiscovery(conn, req, manager)
|
||||
case "bluetooth.stopDiscovery":
|
||||
handleStopDiscovery(conn, req, manager)
|
||||
case "bluetooth.setPowered":
|
||||
handleSetPowered(conn, req, manager)
|
||||
case "bluetooth.pair":
|
||||
handlePairDevice(conn, req, manager)
|
||||
case "bluetooth.connect":
|
||||
handleConnectDevice(conn, req, manager)
|
||||
case "bluetooth.disconnect":
|
||||
handleDisconnectDevice(conn, req, manager)
|
||||
case "bluetooth.remove":
|
||||
handleRemoveDevice(conn, req, manager)
|
||||
case "bluetooth.trust":
|
||||
handleTrustDevice(conn, req, manager)
|
||||
case "bluetooth.untrust":
|
||||
handleUntrustDevice(conn, req, manager)
|
||||
case "bluetooth.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
case "bluetooth.pairing.submit":
|
||||
handlePairingSubmit(conn, req, manager)
|
||||
case "bluetooth.pairing.cancel":
|
||||
handlePairingCancel(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req Request, manager *Manager) {
|
||||
state := manager.GetState()
|
||||
models.Respond(conn, req.ID, state)
|
||||
}
|
||||
|
||||
func handleStartDiscovery(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.StartDiscovery(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery started"})
|
||||
}
|
||||
|
||||
func handleStopDiscovery(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.StopDiscovery(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "discovery stopped"})
|
||||
}
|
||||
|
||||
func handleSetPowered(conn net.Conn, req Request, manager *Manager) {
|
||||
powered, ok := req.Params["powered"].(bool)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'powered' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetPowered(powered); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "powered state updated"})
|
||||
}
|
||||
|
||||
func handlePairDevice(conn net.Conn, req Request, manager *Manager) {
|
||||
devicePath, ok := req.Params["device"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.PairDevice(devicePath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing initiated"})
|
||||
}
|
||||
|
||||
func handleConnectDevice(conn net.Conn, req Request, manager *Manager) {
|
||||
devicePath, ok := req.Params["device"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.ConnectDevice(devicePath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "connecting"})
|
||||
}
|
||||
|
||||
func handleDisconnectDevice(conn net.Conn, req Request, manager *Manager) {
|
||||
devicePath, ok := req.Params["device"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.DisconnectDevice(devicePath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "disconnected"})
|
||||
}
|
||||
|
||||
func handleRemoveDevice(conn net.Conn, req Request, manager *Manager) {
|
||||
devicePath, ok := req.Params["device"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.RemoveDevice(devicePath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device removed"})
|
||||
}
|
||||
|
||||
func handleTrustDevice(conn net.Conn, req Request, manager *Manager) {
|
||||
devicePath, ok := req.Params["device"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.TrustDevice(devicePath, true); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device trusted"})
|
||||
}
|
||||
|
||||
func handleUntrustDevice(conn net.Conn, req Request, manager *Manager) {
|
||||
devicePath, ok := req.Params["device"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'device' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.TrustDevice(devicePath, false); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "device untrusted"})
|
||||
}
|
||||
|
||||
func handlePairingSubmit(conn net.Conn, req Request, manager *Manager) {
|
||||
token, ok := req.Params["token"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
secretsRaw, ok := req.Params["secrets"].(map[string]interface{})
|
||||
secrets := make(map[string]string)
|
||||
if ok {
|
||||
for k, v := range secretsRaw {
|
||||
if str, ok := v.(string); ok {
|
||||
secrets[k] = str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accept := false
|
||||
if acceptParam, ok := req.Params["accept"].(bool); ok {
|
||||
accept = acceptParam
|
||||
}
|
||||
|
||||
if err := manager.SubmitPairing(token, secrets, accept); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing response submitted"})
|
||||
}
|
||||
|
||||
func handlePairingCancel(conn net.Conn, req Request, manager *Manager) {
|
||||
token, ok := req.Params["token"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'token' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.CancelPairing(token); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "pairing cancelled"})
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
event := BluetoothEvent{
|
||||
Type: "state_changed",
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
event := BluetoothEvent{
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[BluetoothEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
41
internal/server/bluez/handlers_test.go
Normal file
41
internal/server/bluez/handlers_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBrokerIntegration(t *testing.T) {
|
||||
broker := NewSubscriptionBroker(nil)
|
||||
ctx := context.Background()
|
||||
|
||||
req := PromptRequest{
|
||||
DevicePath: "/org/bluez/test",
|
||||
DeviceName: "TestDevice",
|
||||
RequestType: "pin",
|
||||
Fields: []string{"pin"},
|
||||
}
|
||||
|
||||
token, err := broker.Ask(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask failed: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
broker.Resolve(token, PromptReply{
|
||||
Secrets: map[string]string{"pin": "1234"},
|
||||
Accept: true,
|
||||
})
|
||||
}()
|
||||
|
||||
reply, err := broker.Wait(ctx, token)
|
||||
if err != nil {
|
||||
t.Fatalf("Wait failed: %v", err)
|
||||
}
|
||||
|
||||
if reply.Secrets["pin"] != "1234" {
|
||||
t.Errorf("expected pin=1234, got %s", reply.Secrets["pin"])
|
||||
}
|
||||
}
|
||||
668
internal/server/bluez/manager.go
Normal file
668
internal/server/bluez/manager.go
Normal file
@@ -0,0 +1,668 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/log"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
adapter1Iface = "org.bluez.Adapter1"
|
||||
objectMgrIface = "org.freedesktop.DBus.ObjectManager"
|
||||
propertiesIface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
state: &BluetoothState{
|
||||
Powered: false,
|
||||
Discovering: false,
|
||||
Devices: []Device{},
|
||||
PairedDevices: []Device{},
|
||||
ConnectedDevices: []Device{},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
subscribers: make(map[string]chan BluetoothState),
|
||||
subMutex: sync.RWMutex{},
|
||||
stopChan: make(chan struct{}),
|
||||
dbusConn: conn,
|
||||
signals: make(chan *dbus.Signal, 256),
|
||||
pairingSubscribers: make(map[string]chan PairingPrompt),
|
||||
pairingSubMutex: sync.RWMutex{},
|
||||
dirty: make(chan struct{}, 1),
|
||||
pendingPairings: make(map[string]bool),
|
||||
eventQueue: make(chan func(), 32),
|
||||
}
|
||||
|
||||
broker := NewSubscriptionBroker(m.broadcastPairingPrompt)
|
||||
m.promptBroker = broker
|
||||
|
||||
adapter, err := m.findAdapter()
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("no bluetooth adapter found: %w", err)
|
||||
}
|
||||
m.adapterPath = adapter
|
||||
|
||||
if err := m.initialize(); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := m.startAgent(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("agent start failed: %w", err)
|
||||
}
|
||||
|
||||
if err := m.startSignalPump(); err != nil {
|
||||
m.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.notifierWg.Add(1)
|
||||
go m.notifier()
|
||||
|
||||
m.eventWg.Add(1)
|
||||
go m.eventWorker()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) findAdapter() (dbus.ObjectPath, error) {
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
|
||||
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
if _, ok := interfaces[adapter1Iface]; ok {
|
||||
log.Infof("[BluezManager] found adapter: %s", path)
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no adapter found")
|
||||
}
|
||||
|
||||
func (m *Manager) initialize() error {
|
||||
if err := m.updateAdapterState(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateDevices(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateAdapterState() error {
|
||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||
|
||||
poweredVar, err := obj.GetProperty(adapter1Iface + ".Powered")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
powered, _ := poweredVar.Value().(bool)
|
||||
|
||||
discoveringVar, err := obj.GetProperty(adapter1Iface + ".Discovering")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discovering, _ := discoveringVar.Value().(bool)
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Powered = powered
|
||||
m.state.Discovering = discovering
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateDevices() error {
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/"))
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
|
||||
if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
devices := []Device{}
|
||||
paired := []Device{}
|
||||
connected := []Device{}
|
||||
|
||||
for path, interfaces := range objects {
|
||||
devProps, ok := interfaces[device1Iface]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(string(path), string(m.adapterPath)+"/") {
|
||||
continue
|
||||
}
|
||||
|
||||
dev := m.deviceFromProps(string(path), devProps)
|
||||
devices = append(devices, dev)
|
||||
|
||||
if dev.Paired {
|
||||
paired = append(paired, dev)
|
||||
}
|
||||
if dev.Connected {
|
||||
connected = append(connected, dev)
|
||||
}
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Devices = devices
|
||||
m.state.PairedDevices = paired
|
||||
m.state.ConnectedDevices = connected
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) deviceFromProps(path string, props map[string]dbus.Variant) Device {
|
||||
dev := Device{Path: path}
|
||||
|
||||
if v, ok := props["Address"]; ok {
|
||||
if addr, ok := v.Value().(string); ok {
|
||||
dev.Address = addr
|
||||
}
|
||||
}
|
||||
if v, ok := props["Name"]; ok {
|
||||
if name, ok := v.Value().(string); ok {
|
||||
dev.Name = name
|
||||
}
|
||||
}
|
||||
if v, ok := props["Alias"]; ok {
|
||||
if alias, ok := v.Value().(string); ok {
|
||||
dev.Alias = alias
|
||||
}
|
||||
}
|
||||
if v, ok := props["Paired"]; ok {
|
||||
if paired, ok := v.Value().(bool); ok {
|
||||
dev.Paired = paired
|
||||
}
|
||||
}
|
||||
if v, ok := props["Trusted"]; ok {
|
||||
if trusted, ok := v.Value().(bool); ok {
|
||||
dev.Trusted = trusted
|
||||
}
|
||||
}
|
||||
if v, ok := props["Blocked"]; ok {
|
||||
if blocked, ok := v.Value().(bool); ok {
|
||||
dev.Blocked = blocked
|
||||
}
|
||||
}
|
||||
if v, ok := props["Connected"]; ok {
|
||||
if connected, ok := v.Value().(bool); ok {
|
||||
dev.Connected = connected
|
||||
}
|
||||
}
|
||||
if v, ok := props["Class"]; ok {
|
||||
if class, ok := v.Value().(uint32); ok {
|
||||
dev.Class = class
|
||||
}
|
||||
}
|
||||
if v, ok := props["Icon"]; ok {
|
||||
if icon, ok := v.Value().(string); ok {
|
||||
dev.Icon = icon
|
||||
}
|
||||
}
|
||||
if v, ok := props["RSSI"]; ok {
|
||||
if rssi, ok := v.Value().(int16); ok {
|
||||
dev.RSSI = rssi
|
||||
}
|
||||
}
|
||||
if v, ok := props["LegacyPairing"]; ok {
|
||||
if legacy, ok := v.Value().(bool); ok {
|
||||
dev.LegacyPairing = legacy
|
||||
}
|
||||
}
|
||||
|
||||
return dev
|
||||
}
|
||||
|
||||
func (m *Manager) startAgent() error {
|
||||
if m.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
agent, err := NewBluezAgent(m.promptBroker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.agent = agent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) startSignalPump() error {
|
||||
m.dbusConn.Signal(m.signals)
|
||||
|
||||
if err := m.dbusConn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(propertiesIface),
|
||||
dbus.WithMatchMember("PropertiesChanged"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.dbusConn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(objectMgrIface),
|
||||
dbus.WithMatchMember("InterfacesAdded"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.dbusConn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(objectMgrIface),
|
||||
dbus.WithMatchMember("InterfacesRemoved"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.sigWG.Add(1)
|
||||
go func() {
|
||||
defer m.sigWG.Done()
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case sig, ok := <-m.signals:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
m.handleSignal(sig)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleSignal(sig *dbus.Signal) {
|
||||
switch sig.Name {
|
||||
case propertiesIface + ".PropertiesChanged":
|
||||
if len(sig.Body) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
iface, ok := sig.Body[0].(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
changed, ok := sig.Body[1].(map[string]dbus.Variant)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch iface {
|
||||
case adapter1Iface:
|
||||
if strings.HasPrefix(string(sig.Path), string(m.adapterPath)) {
|
||||
m.handleAdapterPropertiesChanged(changed)
|
||||
}
|
||||
case device1Iface:
|
||||
m.handleDevicePropertiesChanged(sig.Path, changed)
|
||||
}
|
||||
|
||||
case objectMgrIface + ".InterfacesAdded":
|
||||
m.notifySubscribers()
|
||||
|
||||
case objectMgrIface + ".InterfacesRemoved":
|
||||
m.notifySubscribers()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) handleAdapterPropertiesChanged(changed map[string]dbus.Variant) {
|
||||
m.stateMutex.Lock()
|
||||
dirty := false
|
||||
|
||||
if v, ok := changed["Powered"]; ok {
|
||||
if powered, ok := v.Value().(bool); ok {
|
||||
m.state.Powered = powered
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
if v, ok := changed["Discovering"]; ok {
|
||||
if discovering, ok := v.Value().(bool); ok {
|
||||
m.state.Discovering = discovering
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
if dirty {
|
||||
m.notifySubscribers()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed map[string]dbus.Variant) {
|
||||
pairedVar, hasPaired := changed["Paired"]
|
||||
_, hasConnected := changed["Connected"]
|
||||
_, hasTrusted := changed["Trusted"]
|
||||
|
||||
if hasPaired {
|
||||
if paired, ok := pairedVar.Value().(bool); ok && paired {
|
||||
devicePath := string(path)
|
||||
m.pendingPairingsMux.Lock()
|
||||
wasPending := m.pendingPairings[devicePath]
|
||||
if wasPending {
|
||||
delete(m.pendingPairings, devicePath)
|
||||
}
|
||||
m.pendingPairingsMux.Unlock()
|
||||
|
||||
if wasPending {
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||
if err := m.ConnectDevice(devicePath); err != nil {
|
||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||
}
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasPaired || hasConnected || hasTrusted {
|
||||
select {
|
||||
case m.eventQueue <- func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
m.updateDevices()
|
||||
m.notifySubscribers()
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) eventWorker() {
|
||||
defer m.eventWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case event := <-m.eventQueue:
|
||||
event()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) notifier() {
|
||||
defer m.notifierWg.Done()
|
||||
const minGap = 200 * time.Millisecond
|
||||
timer := time.NewTimer(minGap)
|
||||
timer.Stop()
|
||||
var pending bool
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-m.dirty:
|
||||
if pending {
|
||||
continue
|
||||
}
|
||||
pending = true
|
||||
timer.Reset(minGap)
|
||||
case <-timer.C:
|
||||
if !pending {
|
||||
continue
|
||||
}
|
||||
m.updateDevices()
|
||||
|
||||
m.subMutex.RLock()
|
||||
if len(m.subscribers) == 0 {
|
||||
m.subMutex.RUnlock()
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
currentState := m.snapshotState()
|
||||
|
||||
if m.lastNotifiedState != nil && !stateChanged(m.lastNotifiedState, ¤tState) {
|
||||
m.subMutex.RUnlock()
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ch := range m.subscribers {
|
||||
select {
|
||||
case ch <- currentState:
|
||||
default:
|
||||
}
|
||||
}
|
||||
m.subMutex.RUnlock()
|
||||
|
||||
stateCopy := currentState
|
||||
m.lastNotifiedState = &stateCopy
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() BluetoothState {
|
||||
return m.snapshotState()
|
||||
}
|
||||
|
||||
func (m *Manager) snapshotState() BluetoothState {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
|
||||
s := *m.state
|
||||
s.Devices = append([]Device(nil), m.state.Devices...)
|
||||
s.PairedDevices = append([]Device(nil), m.state.PairedDevices...)
|
||||
s.ConnectedDevices = append([]Device(nil), m.state.ConnectedDevices...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan BluetoothState {
|
||||
ch := make(chan BluetoothState, 64)
|
||||
m.subMutex.Lock()
|
||||
m.subscribers[id] = ch
|
||||
m.subMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subMutex.Lock()
|
||||
if ch, ok := m.subscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.subscribers, id)
|
||||
}
|
||||
m.subMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) SubscribePairing(id string) chan PairingPrompt {
|
||||
ch := make(chan PairingPrompt, 16)
|
||||
m.pairingSubMutex.Lock()
|
||||
m.pairingSubscribers[id] = ch
|
||||
m.pairingSubMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) UnsubscribePairing(id string) {
|
||||
m.pairingSubMutex.Lock()
|
||||
if ch, ok := m.pairingSubscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.pairingSubscribers, id)
|
||||
}
|
||||
m.pairingSubMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) broadcastPairingPrompt(prompt PairingPrompt) {
|
||||
m.pairingSubMutex.RLock()
|
||||
defer m.pairingSubMutex.RUnlock()
|
||||
|
||||
for _, ch := range m.pairingSubscribers {
|
||||
select {
|
||||
case ch <- prompt:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SubmitPairing(token string, secrets map[string]string, accept bool) error {
|
||||
if m.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
return m.promptBroker.Resolve(token, PromptReply{
|
||||
Secrets: secrets,
|
||||
Accept: accept,
|
||||
Cancel: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) CancelPairing(token string) error {
|
||||
if m.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
return m.promptBroker.Resolve(token, PromptReply{
|
||||
Cancel: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) StartDiscovery() error {
|
||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||
return obj.Call(adapter1Iface+".StartDiscovery", 0).Err
|
||||
}
|
||||
|
||||
func (m *Manager) StopDiscovery() error {
|
||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||
return obj.Call(adapter1Iface+".StopDiscovery", 0).Err
|
||||
}
|
||||
|
||||
func (m *Manager) SetPowered(powered bool) error {
|
||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||
return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err
|
||||
}
|
||||
|
||||
func (m *Manager) PairDevice(devicePath string) error {
|
||||
m.pendingPairingsMux.Lock()
|
||||
m.pendingPairings[devicePath] = true
|
||||
m.pendingPairingsMux.Unlock()
|
||||
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
|
||||
err := obj.Call(device1Iface+".Pair", 0).Err
|
||||
|
||||
if err != nil {
|
||||
m.pendingPairingsMux.Lock()
|
||||
delete(m.pendingPairings, devicePath)
|
||||
m.pendingPairingsMux.Unlock()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) ConnectDevice(devicePath string) error {
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
|
||||
return obj.Call(device1Iface+".Connect", 0).Err
|
||||
}
|
||||
|
||||
func (m *Manager) DisconnectDevice(devicePath string) error {
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
|
||||
return obj.Call(device1Iface+".Disconnect", 0).Err
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveDevice(devicePath string) error {
|
||||
obj := m.dbusConn.Object(bluezService, m.adapterPath)
|
||||
return obj.Call(adapter1Iface+".RemoveDevice", 0, dbus.ObjectPath(devicePath)).Err
|
||||
}
|
||||
|
||||
func (m *Manager) TrustDevice(devicePath string, trusted bool) error {
|
||||
obj := m.dbusConn.Object(bluezService, dbus.ObjectPath(devicePath))
|
||||
return obj.Call(propertiesIface+".Set", 0, device1Iface, "Trusted", dbus.MakeVariant(trusted)).Err
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
m.notifierWg.Wait()
|
||||
m.eventWg.Wait()
|
||||
|
||||
m.sigWG.Wait()
|
||||
|
||||
if m.signals != nil {
|
||||
m.dbusConn.RemoveSignal(m.signals)
|
||||
close(m.signals)
|
||||
}
|
||||
|
||||
if m.agent != nil {
|
||||
m.agent.Close()
|
||||
}
|
||||
|
||||
m.subMutex.Lock()
|
||||
for _, ch := range m.subscribers {
|
||||
close(ch)
|
||||
}
|
||||
m.subscribers = make(map[string]chan BluetoothState)
|
||||
m.subMutex.Unlock()
|
||||
|
||||
m.pairingSubMutex.Lock()
|
||||
for _, ch := range m.pairingSubscribers {
|
||||
close(ch)
|
||||
}
|
||||
m.pairingSubscribers = make(map[string]chan PairingPrompt)
|
||||
m.pairingSubMutex.Unlock()
|
||||
|
||||
if m.dbusConn != nil {
|
||||
m.dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func stateChanged(old, new *BluetoothState) bool {
|
||||
if old.Powered != new.Powered {
|
||||
return true
|
||||
}
|
||||
if old.Discovering != new.Discovering {
|
||||
return true
|
||||
}
|
||||
if len(old.Devices) != len(new.Devices) {
|
||||
return true
|
||||
}
|
||||
if len(old.PairedDevices) != len(new.PairedDevices) {
|
||||
return true
|
||||
}
|
||||
if len(old.ConnectedDevices) != len(new.ConnectedDevices) {
|
||||
return true
|
||||
}
|
||||
for i := range old.Devices {
|
||||
if old.Devices[i].Path != new.Devices[i].Path {
|
||||
return true
|
||||
}
|
||||
if old.Devices[i].Paired != new.Devices[i].Paired {
|
||||
return true
|
||||
}
|
||||
if old.Devices[i].Connected != new.Devices[i].Connected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
99
internal/server/bluez/subscription_broker.go
Normal file
99
internal/server/bluez/subscription_broker.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/errdefs"
|
||||
)
|
||||
|
||||
type SubscriptionBroker struct {
|
||||
mu sync.RWMutex
|
||||
pending map[string]chan PromptReply
|
||||
requests map[string]PromptRequest
|
||||
broadcastPrompt func(PairingPrompt)
|
||||
}
|
||||
|
||||
func NewSubscriptionBroker(broadcastPrompt func(PairingPrompt)) PromptBroker {
|
||||
return &SubscriptionBroker{
|
||||
pending: make(map[string]chan PromptReply),
|
||||
requests: make(map[string]PromptRequest),
|
||||
broadcastPrompt: broadcastPrompt,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Ask(ctx context.Context, req PromptRequest) (string, error) {
|
||||
token, err := generateToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
replyChan := make(chan PromptReply, 1)
|
||||
b.mu.Lock()
|
||||
b.pending[token] = replyChan
|
||||
b.requests[token] = req
|
||||
b.mu.Unlock()
|
||||
|
||||
if b.broadcastPrompt != nil {
|
||||
prompt := PairingPrompt{
|
||||
Token: token,
|
||||
DevicePath: req.DevicePath,
|
||||
DeviceName: req.DeviceName,
|
||||
DeviceAddr: req.DeviceAddr,
|
||||
RequestType: req.RequestType,
|
||||
Fields: req.Fields,
|
||||
Hints: req.Hints,
|
||||
Passkey: req.Passkey,
|
||||
}
|
||||
b.broadcastPrompt(prompt)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Wait(ctx context.Context, token string) (PromptReply, error) {
|
||||
b.mu.RLock()
|
||||
replyChan, exists := b.pending[token]
|
||||
b.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return PromptReply{}, fmt.Errorf("unknown token: %s", token)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
b.cleanup(token)
|
||||
return PromptReply{}, errdefs.ErrSecretPromptTimeout
|
||||
case reply := <-replyChan:
|
||||
b.cleanup(token)
|
||||
if reply.Cancel {
|
||||
return reply, errdefs.ErrSecretPromptCancelled
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) Resolve(token string, reply PromptReply) error {
|
||||
b.mu.RLock()
|
||||
replyChan, exists := b.pending[token]
|
||||
b.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown or expired token: %s", token)
|
||||
}
|
||||
|
||||
select {
|
||||
case replyChan <- reply:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("failed to deliver reply for token: %s", token)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SubscriptionBroker) cleanup(token string) {
|
||||
b.mu.Lock()
|
||||
delete(b.pending, token)
|
||||
delete(b.requests, token)
|
||||
b.mu.Unlock()
|
||||
}
|
||||
80
internal/server/bluez/types.go
Normal file
80
internal/server/bluez/types.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type BluetoothState struct {
|
||||
Powered bool `json:"powered"`
|
||||
Discovering bool `json:"discovering"`
|
||||
Devices []Device `json:"devices"`
|
||||
PairedDevices []Device `json:"pairedDevices"`
|
||||
ConnectedDevices []Device `json:"connectedDevices"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
Path string `json:"path"`
|
||||
Address string `json:"address"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Paired bool `json:"paired"`
|
||||
Trusted bool `json:"trusted"`
|
||||
Blocked bool `json:"blocked"`
|
||||
Connected bool `json:"connected"`
|
||||
Class uint32 `json:"class"`
|
||||
Icon string `json:"icon"`
|
||||
RSSI int16 `json:"rssi"`
|
||||
LegacyPairing bool `json:"legacyPairing"`
|
||||
}
|
||||
|
||||
type PromptRequest struct {
|
||||
DevicePath string `json:"devicePath"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
DeviceAddr string `json:"deviceAddr"`
|
||||
RequestType string `json:"requestType"`
|
||||
Fields []string `json:"fields"`
|
||||
Hints []string `json:"hints"`
|
||||
Passkey *uint32 `json:"passkey,omitempty"`
|
||||
}
|
||||
|
||||
type PromptReply struct {
|
||||
Secrets map[string]string `json:"secrets"`
|
||||
Accept bool `json:"accept"`
|
||||
Cancel bool `json:"cancel"`
|
||||
}
|
||||
|
||||
type PairingPrompt struct {
|
||||
Token string `json:"token"`
|
||||
DevicePath string `json:"devicePath"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
DeviceAddr string `json:"deviceAddr"`
|
||||
RequestType string `json:"requestType"`
|
||||
Fields []string `json:"fields"`
|
||||
Hints []string `json:"hints"`
|
||||
Passkey *uint32 `json:"passkey,omitempty"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *BluetoothState
|
||||
stateMutex sync.RWMutex
|
||||
subscribers map[string]chan BluetoothState
|
||||
subMutex sync.RWMutex
|
||||
stopChan chan struct{}
|
||||
dbusConn *dbus.Conn
|
||||
signals chan *dbus.Signal
|
||||
sigWG sync.WaitGroup
|
||||
agent *BluezAgent
|
||||
promptBroker PromptBroker
|
||||
pairingSubscribers map[string]chan PairingPrompt
|
||||
pairingSubMutex sync.RWMutex
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotifiedState *BluetoothState
|
||||
adapterPath dbus.ObjectPath
|
||||
pendingPairings map[string]bool
|
||||
pendingPairingsMux sync.Mutex
|
||||
eventQueue chan func()
|
||||
eventWg sync.WaitGroup
|
||||
}
|
||||
210
internal/server/bluez/types_test.go
Normal file
210
internal/server/bluez/types_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package bluez
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBluetoothStateJSON(t *testing.T) {
|
||||
state := BluetoothState{
|
||||
Powered: true,
|
||||
Discovering: false,
|
||||
Devices: []Device{
|
||||
{
|
||||
Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF",
|
||||
Address: "AA:BB:CC:DD:EE:FF",
|
||||
Name: "TestDevice",
|
||||
Alias: "My Device",
|
||||
Paired: true,
|
||||
Trusted: false,
|
||||
Connected: true,
|
||||
Class: 0x240418,
|
||||
Icon: "audio-headset",
|
||||
RSSI: -50,
|
||||
},
|
||||
},
|
||||
PairedDevices: []Device{
|
||||
{
|
||||
Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF",
|
||||
Address: "AA:BB:CC:DD:EE:FF",
|
||||
Paired: true,
|
||||
},
|
||||
},
|
||||
ConnectedDevices: []Device{
|
||||
{
|
||||
Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF",
|
||||
Address: "AA:BB:CC:DD:EE:FF",
|
||||
Connected: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal state: %v", err)
|
||||
}
|
||||
|
||||
var decoded BluetoothState
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal state: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Powered != state.Powered {
|
||||
t.Errorf("expected Powered=%v, got %v", state.Powered, decoded.Powered)
|
||||
}
|
||||
|
||||
if len(decoded.Devices) != 1 {
|
||||
t.Fatalf("expected 1 device, got %d", len(decoded.Devices))
|
||||
}
|
||||
|
||||
if decoded.Devices[0].Address != "AA:BB:CC:DD:EE:FF" {
|
||||
t.Errorf("expected address AA:BB:CC:DD:EE:FF, got %s", decoded.Devices[0].Address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceJSON(t *testing.T) {
|
||||
device := Device{
|
||||
Path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF",
|
||||
Address: "AA:BB:CC:DD:EE:FF",
|
||||
Name: "TestDevice",
|
||||
Alias: "My Device",
|
||||
Paired: true,
|
||||
Trusted: true,
|
||||
Blocked: false,
|
||||
Connected: true,
|
||||
Class: 0x240418,
|
||||
Icon: "audio-headset",
|
||||
RSSI: -50,
|
||||
LegacyPairing: false,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(device)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal device: %v", err)
|
||||
}
|
||||
|
||||
var decoded Device
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal device: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Address != device.Address {
|
||||
t.Errorf("expected Address=%s, got %s", device.Address, decoded.Address)
|
||||
}
|
||||
|
||||
if decoded.Name != device.Name {
|
||||
t.Errorf("expected Name=%s, got %s", device.Name, decoded.Name)
|
||||
}
|
||||
|
||||
if decoded.Paired != device.Paired {
|
||||
t.Errorf("expected Paired=%v, got %v", device.Paired, decoded.Paired)
|
||||
}
|
||||
|
||||
if decoded.RSSI != device.RSSI {
|
||||
t.Errorf("expected RSSI=%d, got %d", device.RSSI, decoded.RSSI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairingPromptJSON(t *testing.T) {
|
||||
passkey := uint32(123456)
|
||||
prompt := PairingPrompt{
|
||||
Token: "test-token",
|
||||
DevicePath: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF",
|
||||
DeviceName: "TestDevice",
|
||||
DeviceAddr: "AA:BB:CC:DD:EE:FF",
|
||||
RequestType: "confirm",
|
||||
Fields: []string{"decision"},
|
||||
Hints: []string{},
|
||||
Passkey: &passkey,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(prompt)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal prompt: %v", err)
|
||||
}
|
||||
|
||||
var decoded PairingPrompt
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal prompt: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Token != prompt.Token {
|
||||
t.Errorf("expected Token=%s, got %s", prompt.Token, decoded.Token)
|
||||
}
|
||||
|
||||
if decoded.DeviceName != prompt.DeviceName {
|
||||
t.Errorf("expected DeviceName=%s, got %s", prompt.DeviceName, decoded.DeviceName)
|
||||
}
|
||||
|
||||
if decoded.Passkey == nil {
|
||||
t.Fatal("expected non-nil Passkey")
|
||||
}
|
||||
|
||||
if *decoded.Passkey != *prompt.Passkey {
|
||||
t.Errorf("expected Passkey=%d, got %d", *prompt.Passkey, *decoded.Passkey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptReplyJSON(t *testing.T) {
|
||||
reply := PromptReply{
|
||||
Secrets: map[string]string{
|
||||
"pin": "1234",
|
||||
"passkey": "567890",
|
||||
},
|
||||
Accept: true,
|
||||
Cancel: false,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(reply)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal reply: %v", err)
|
||||
}
|
||||
|
||||
var decoded PromptReply
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal reply: %v", err)
|
||||
}
|
||||
|
||||
if decoded.Secrets["pin"] != reply.Secrets["pin"] {
|
||||
t.Errorf("expected pin=%s, got %s", reply.Secrets["pin"], decoded.Secrets["pin"])
|
||||
}
|
||||
|
||||
if decoded.Accept != reply.Accept {
|
||||
t.Errorf("expected Accept=%v, got %v", reply.Accept, decoded.Accept)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptRequestJSON(t *testing.T) {
|
||||
passkey := uint32(123456)
|
||||
req := PromptRequest{
|
||||
DevicePath: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF",
|
||||
DeviceName: "TestDevice",
|
||||
DeviceAddr: "AA:BB:CC:DD:EE:FF",
|
||||
RequestType: "confirm",
|
||||
Fields: []string{"decision"},
|
||||
Hints: []string{"hint1", "hint2"},
|
||||
Passkey: &passkey,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
var decoded PromptRequest
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatalf("failed to unmarshal request: %v", err)
|
||||
}
|
||||
|
||||
if decoded.DevicePath != req.DevicePath {
|
||||
t.Errorf("expected DevicePath=%s, got %s", req.DevicePath, decoded.DevicePath)
|
||||
}
|
||||
|
||||
if decoded.RequestType != req.RequestType {
|
||||
t.Errorf("expected RequestType=%s, got %s", req.RequestType, decoded.RequestType)
|
||||
}
|
||||
|
||||
if len(decoded.Fields) != len(req.Fields) {
|
||||
t.Errorf("expected %d fields, got %d", len(req.Fields), len(decoded.Fields))
|
||||
}
|
||||
}
|
||||
485
internal/server/brightness/ddc.go
Normal file
485
internal/server/brightness/ddc.go
Normal file
@@ -0,0 +1,485 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/log"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
I2C_SLAVE = 0x0703
|
||||
DDCCI_ADDR = 0x37
|
||||
DDCCI_VCP_GET = 0x01
|
||||
DDCCI_VCP_SET = 0x03
|
||||
VCP_BRIGHTNESS = 0x10
|
||||
DDC_SOURCE_ADDR = 0x51
|
||||
)
|
||||
|
||||
func NewDDCBackend() (*DDCBackend, error) {
|
||||
b := &DDCBackend{
|
||||
devices: make(map[string]*ddcDevice),
|
||||
scanInterval: 30 * time.Second,
|
||||
debounceTimers: make(map[string]*time.Timer),
|
||||
debouncePending: make(map[string]ddcPendingSet),
|
||||
}
|
||||
|
||||
if err := b.scanI2CDevices(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) scanI2CDevices() error {
|
||||
b.scanMutex.Lock()
|
||||
lastScan := b.lastScan
|
||||
b.scanMutex.Unlock()
|
||||
|
||||
if time.Since(lastScan) < b.scanInterval {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.scanMutex.Lock()
|
||||
defer b.scanMutex.Unlock()
|
||||
|
||||
if time.Since(b.lastScan) < b.scanInterval {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.devicesMutex.Lock()
|
||||
defer b.devicesMutex.Unlock()
|
||||
|
||||
b.devices = make(map[string]*ddcDevice)
|
||||
|
||||
for i := 0; i < 32; i++ {
|
||||
busPath := fmt.Sprintf("/dev/i2c-%d", i)
|
||||
if _, err := os.Stat(busPath); os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip SMBus, GPU internal buses (e.g. AMDGPU SMU) to prevent GPU hangs
|
||||
if isIgnorableI2CBus(i) {
|
||||
log.Debugf("Skipping ignorable i2c-%d", i)
|
||||
continue
|
||||
}
|
||||
|
||||
dev, err := b.probeDDCDevice(i)
|
||||
if err != nil || dev == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
id := fmt.Sprintf("ddc:i2c-%d", i)
|
||||
dev.id = id
|
||||
b.devices[id] = dev
|
||||
log.Debugf("found DDC device on i2c-%d", i)
|
||||
}
|
||||
|
||||
b.lastScan = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) probeDDCDevice(bus int) (*ddcDevice, error) {
|
||||
busPath := fmt.Sprintf("/dev/i2c-%d", bus)
|
||||
|
||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
dummy := make([]byte, 32)
|
||||
syscall.Read(fd, dummy)
|
||||
|
||||
writebuf := []byte{0x00}
|
||||
n, err := syscall.Write(fd, writebuf)
|
||||
if err == nil && n == len(writebuf) {
|
||||
name := b.getDDCName(bus)
|
||||
dev := &ddcDevice{
|
||||
bus: bus,
|
||||
addr: DDCCI_ADDR,
|
||||
name: name,
|
||||
}
|
||||
b.readInitialBrightness(fd, dev)
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
readbuf := make([]byte, 4)
|
||||
n, err = syscall.Read(fd, readbuf)
|
||||
if err != nil || n == 0 {
|
||||
return nil, fmt.Errorf("x37 unresponsive")
|
||||
}
|
||||
|
||||
name := b.getDDCName(bus)
|
||||
|
||||
dev := &ddcDevice{
|
||||
bus: bus,
|
||||
addr: DDCCI_ADDR,
|
||||
name: name,
|
||||
}
|
||||
b.readInitialBrightness(fd, dev)
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) getDDCName(bus int) string {
|
||||
sysfsPath := fmt.Sprintf("/sys/class/i2c-adapter/i2c-%d/name", bus)
|
||||
data, err := os.ReadFile(sysfsPath)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("I2C-%d", bus)
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(string(data))
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("I2C-%d", bus)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (b *DDCBackend) readInitialBrightness(fd int, dev *ddcDevice) {
|
||||
cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS)
|
||||
if err != nil {
|
||||
log.Debugf("failed to read initial brightness for %s: %v", dev.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
dev.max = cap.max
|
||||
dev.lastBrightness = cap.current
|
||||
log.Debugf("initialized %s with brightness %d/%d", dev.name, cap.current, cap.max)
|
||||
}
|
||||
|
||||
func (b *DDCBackend) GetDevices() ([]Device, error) {
|
||||
if err := b.scanI2CDevices(); err != nil {
|
||||
log.Debugf("DDC scan error: %v", err)
|
||||
}
|
||||
|
||||
b.devicesMutex.Lock()
|
||||
defer b.devicesMutex.Unlock()
|
||||
|
||||
devices := make([]Device, 0, len(b.devices))
|
||||
|
||||
for id, dev := range b.devices {
|
||||
devices = append(devices, Device{
|
||||
Class: ClassDDC,
|
||||
ID: id,
|
||||
Name: dev.name,
|
||||
Current: dev.lastBrightness,
|
||||
Max: dev.max,
|
||||
CurrentPercent: dev.lastBrightness,
|
||||
Backend: "ddc",
|
||||
})
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) SetBrightness(id string, value int, exponential bool, callback func()) error {
|
||||
return b.SetBrightnessWithExponent(id, value, exponential, 1.2, callback)
|
||||
}
|
||||
|
||||
func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential bool, exponent float64, callback func()) error {
|
||||
b.devicesMutex.RLock()
|
||||
_, ok := b.devices[id]
|
||||
b.devicesMutex.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("device not found: %s", id)
|
||||
}
|
||||
|
||||
if value < 0 {
|
||||
return fmt.Errorf("value out of range: %d", value)
|
||||
}
|
||||
|
||||
b.debounceMutex.Lock()
|
||||
defer b.debounceMutex.Unlock()
|
||||
|
||||
b.debouncePending[id] = ddcPendingSet{
|
||||
percent: value,
|
||||
callback: callback,
|
||||
}
|
||||
|
||||
if timer, exists := b.debounceTimers[id]; exists {
|
||||
timer.Reset(200 * time.Millisecond)
|
||||
} else {
|
||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||
b.debounceMutex.Lock()
|
||||
pending, exists := b.debouncePending[id]
|
||||
if exists {
|
||||
delete(b.debouncePending, id)
|
||||
}
|
||||
b.debounceMutex.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
err := b.setBrightnessImmediateWithExponent(id, pending.percent, exponential, exponent)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||
}
|
||||
|
||||
if pending.callback != nil {
|
||||
pending.callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) setBrightnessImmediate(id string, value int, exponential bool) error {
|
||||
return b.setBrightnessImmediateWithExponent(id, value, exponential, 1.2)
|
||||
}
|
||||
|
||||
func (b *DDCBackend) setBrightnessImmediateWithExponent(id string, value int, exponential bool, exponent float64) error {
|
||||
b.devicesMutex.RLock()
|
||||
dev, ok := b.devices[id]
|
||||
b.devicesMutex.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("device not found: %s", id)
|
||||
}
|
||||
|
||||
busPath := fmt.Sprintf("/dev/i2c-%d", dev.bus)
|
||||
|
||||
fd, err := syscall.Open(busPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open i2c device: %w", err)
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(dev.addr)); errno != 0 {
|
||||
return fmt.Errorf("set i2c slave addr: %w", errno)
|
||||
}
|
||||
|
||||
max := dev.max
|
||||
if max == 0 {
|
||||
cap, err := b.getVCPFeature(fd, VCP_BRIGHTNESS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current capability: %w", err)
|
||||
}
|
||||
max = cap.max
|
||||
b.devicesMutex.Lock()
|
||||
dev.max = max
|
||||
b.devicesMutex.Unlock()
|
||||
}
|
||||
|
||||
if err := b.setVCPFeature(fd, VCP_BRIGHTNESS, value); err != nil {
|
||||
return fmt.Errorf("set vcp feature: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("set %s to %d/%d", id, value, max)
|
||||
|
||||
b.devicesMutex.Lock()
|
||||
dev.max = max
|
||||
dev.lastBrightness = value
|
||||
b.devicesMutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) getVCPFeature(fd int, vcp byte) (*ddcCapability, error) {
|
||||
for flushTry := 0; flushTry < 3; flushTry++ {
|
||||
dummy := make([]byte, 32)
|
||||
n, _ := syscall.Read(fd, dummy)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
|
||||
data := []byte{
|
||||
DDCCI_VCP_GET,
|
||||
vcp,
|
||||
}
|
||||
|
||||
payload := []byte{
|
||||
DDC_SOURCE_ADDR,
|
||||
byte(len(data)) | 0x80,
|
||||
}
|
||||
payload = append(payload, data...)
|
||||
payload = append(payload, ddcciChecksum(payload))
|
||||
|
||||
n, err := syscall.Write(fd, payload)
|
||||
if err != nil || n != len(payload) {
|
||||
return nil, fmt.Errorf("write i2c: %w", err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
pollFds := []unix.PollFd{
|
||||
{
|
||||
Fd: int32(fd),
|
||||
Events: unix.POLLIN,
|
||||
},
|
||||
}
|
||||
|
||||
pollTimeout := 200
|
||||
pollResult, err := unix.Poll(pollFds, pollTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("poll i2c: %w", err)
|
||||
}
|
||||
if pollResult == 0 {
|
||||
return nil, fmt.Errorf("poll timeout after %dms", pollTimeout)
|
||||
}
|
||||
if pollFds[0].Revents&unix.POLLIN == 0 {
|
||||
return nil, fmt.Errorf("poll returned but POLLIN not set")
|
||||
}
|
||||
|
||||
response := make([]byte, 12)
|
||||
n, err = syscall.Read(fd, response)
|
||||
if err != nil || n < 8 {
|
||||
return nil, fmt.Errorf("read i2c: %w", err)
|
||||
}
|
||||
|
||||
if response[0] != 0x6E || response[2] != 0x02 {
|
||||
return nil, fmt.Errorf("invalid ddc response")
|
||||
}
|
||||
|
||||
resultCode := response[3]
|
||||
if resultCode != 0x00 {
|
||||
return nil, fmt.Errorf("vcp feature not supported")
|
||||
}
|
||||
|
||||
responseVCP := response[4]
|
||||
if responseVCP != vcp {
|
||||
return nil, fmt.Errorf("vcp mismatch: wanted 0x%02x, got 0x%02x", vcp, responseVCP)
|
||||
}
|
||||
|
||||
maxHigh := response[6]
|
||||
maxLow := response[7]
|
||||
currentHigh := response[8]
|
||||
currentLow := response[9]
|
||||
|
||||
max := int(binary.BigEndian.Uint16([]byte{maxHigh, maxLow}))
|
||||
current := int(binary.BigEndian.Uint16([]byte{currentHigh, currentLow}))
|
||||
|
||||
return &ddcCapability{
|
||||
vcp: vcp,
|
||||
max: max,
|
||||
current: current,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ddcciChecksum(payload []byte) byte {
|
||||
sum := byte(0x6E)
|
||||
for _, b := range payload {
|
||||
sum ^= b
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (b *DDCBackend) setVCPFeature(fd int, vcp byte, value int) error {
|
||||
data := []byte{
|
||||
DDCCI_VCP_SET,
|
||||
vcp,
|
||||
byte(value >> 8),
|
||||
byte(value & 0xFF),
|
||||
}
|
||||
|
||||
payload := []byte{
|
||||
DDC_SOURCE_ADDR,
|
||||
byte(len(data)) | 0x80,
|
||||
}
|
||||
payload = append(payload, data...)
|
||||
payload = append(payload, ddcciChecksum(payload))
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), I2C_SLAVE, uintptr(DDCCI_ADDR)); errno != 0 {
|
||||
return fmt.Errorf("set i2c slave for write: %w", errno)
|
||||
}
|
||||
|
||||
n, err := syscall.Write(fd, payload)
|
||||
if err != nil || n != len(payload) {
|
||||
return fmt.Errorf("write i2c: wrote %d/%d: %w", n, len(payload), err)
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *DDCBackend) percentToValue(percent int, max int, exponential bool) int {
|
||||
const minValue = 1
|
||||
|
||||
if percent == 0 {
|
||||
return minValue
|
||||
}
|
||||
|
||||
usableRange := max - minValue
|
||||
var value int
|
||||
|
||||
if exponential {
|
||||
const exponent = 2.0
|
||||
normalizedPercent := float64(percent) / 100.0
|
||||
hardwarePercent := math.Pow(normalizedPercent, 1.0/exponent)
|
||||
value = minValue + int(math.Round(hardwarePercent*float64(usableRange)))
|
||||
} else {
|
||||
value = minValue + ((percent - 1) * usableRange / 99)
|
||||
}
|
||||
|
||||
if value < minValue {
|
||||
value = minValue
|
||||
}
|
||||
if value > max {
|
||||
value = max
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
|
||||
const minValue = 1
|
||||
|
||||
if max == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if value <= minValue {
|
||||
return 1
|
||||
}
|
||||
|
||||
usableRange := max - minValue
|
||||
if usableRange == 0 {
|
||||
return 100
|
||||
}
|
||||
|
||||
var percent int
|
||||
|
||||
if exponential {
|
||||
const exponent = 2.0
|
||||
linearPercent := 1 + ((value - minValue) * 99 / usableRange)
|
||||
normalizedLinear := float64(linearPercent) / 100.0
|
||||
expPercent := math.Pow(normalizedLinear, exponent)
|
||||
percent = int(math.Round(expPercent * 100.0))
|
||||
} else {
|
||||
percent = 1 + ((value - minValue) * 99 / usableRange)
|
||||
}
|
||||
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
if percent < 1 {
|
||||
percent = 1
|
||||
}
|
||||
|
||||
return percent
|
||||
}
|
||||
|
||||
func (b *DDCBackend) Close() {
|
||||
}
|
||||
|
||||
var _ = unsafe.Sizeof(0)
|
||||
var _ = filepath.Join
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user