From 6a49f907d8b0dfce6832d0414fc7bc6b4a55efbd Mon Sep 17 00:00:00 2001 From: SouthWolf Date: Wed, 27 Nov 2024 05:12:33 +0800 Subject: [PATCH] fix: improve windows compatibility * Windows compatibility - Updated GetHomeDir() and notes for running with Admininistrator on Windows to create symlinks. * fix: remove new image from git * fix: remove new image from git --------- Co-authored-by: Sam --- README.md | 7 ++++++ config/config.go | 22 ++++++---------- helpers.go | 8 +++--- main.go | 10 +++++--- operations.go | 5 ++-- utils/utils.go | 28 +++++++++++++++++++++ utils/utils_test.go | 46 ++++++++++++++++++++++++++++++++++ vramestimator/vramestimator.go | 5 ++-- 8 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 utils/utils.go create mode 100644 utils/utils_test.go diff --git a/README.md b/README.md index 99934b0..868f6eb 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The application allows users to interactively select models, sort, filter, edit, - [Key Bindings](#key-bindings) - [Top](#top) - [Inspect](#inspect) + - [Link](#link) - [Command-line Options](#command-line-options) - [Configuration](#configuration) - [Installation and build from source](#installation-and-build-from-source) @@ -128,6 +129,12 @@ Inspect (`i`) ![](screenshots/gollama-inspect.png) +#### Link + +Link (`l`) and Link All (`L`) + +Note: Requires Admin privileges if you're running Windows. + #### Command-line Options - `-l`: List all available Ollama models and exit diff --git a/config/config.go b/config/config.go index 67a36b0..9ecc80b 100644 --- a/config/config.go +++ b/config/config.go @@ -7,7 +7,7 @@ import ( "time" "github.com/fsnotify/fsnotify" - "github.com/sammcj/gollama/logging" + "github.com/sammcj/gollama/utils" "github.com/spf13/viper" ) @@ -72,7 +72,8 @@ func CreateDefaultConfig() error { func LoadConfig() (Config, error) { viper.SetConfigName("config") viper.SetConfigType("json") - viper.AddConfigPath(filepath.Join(os.Getenv("HOME"), ".config", "gollama")) + // Dir of config file + viper.AddConfigPath(utils.GetConfigDir()) // Read the config file if err := viper.ReadInConfig(); err != nil { @@ -80,7 +81,7 @@ func LoadConfig() (Config, error) { // Config file not found; create it if err := CreateDefaultConfig(); err != nil { // if the file already exists - return it, otherwise throw an error - if _, err := os.Stat(getConfigPath()); err == nil { + if _, err := os.Stat(utils.GetConfigPath()); err == nil { return LoadConfig() } return Config{}, fmt.Errorf("failed to create default config: %w", err) @@ -88,8 +89,8 @@ func LoadConfig() (Config, error) { } else { // if the config file is borked, recreate it and let the user know if err := CreateDefaultConfig(); err != nil { - backupPath := getConfigPath() + ".borked." + time.Now().Format("2006-01-02") - if err := os.Rename(getConfigPath(), backupPath); err != nil { + backupPath := utils.GetConfigPath() + ".borked." + time.Now().Format("2006-01-02") + if err := os.Rename(utils.GetConfigPath(), backupPath); err != nil { return Config{}, fmt.Errorf("failed to rename config file: %w", err) } if err := CreateDefaultConfig(); err != nil { @@ -119,7 +120,7 @@ func SaveConfig(config Config) error { viper.Set("sort_order", config.SortOrder) } - configPath := getConfigPath() + configPath := utils.GetConfigPath() if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) } @@ -141,12 +142,3 @@ func (c *Config) SaveIfModified() error { func (c *Config) SetModified() { c.modified = true } - -// getConfigPath returns the path to the configuration JSON file. -func getConfigPath() string { - homeDir, err := os.UserHomeDir() - if err != nil { - logging.ErrorLogger.Printf("Failed to get user home directory: %v\n", err) - } - return filepath.Join(homeDir, ".config", "gollama", "config.json") -} diff --git a/helpers.go b/helpers.go index 4c03c34..c186624 100644 --- a/helpers.go +++ b/helpers.go @@ -135,10 +135,10 @@ func listModels(models []Model) { os.Exit(1) } - if len(models) == 0 { - fmt.Println("No models available to display.") - return - } + if len(models) == 0 { + fmt.Println("No models available to display.") + return + } stripString := cfg.StripString nameWidth, sizeWidth, quantWidth, modifiedWidth, idWidth, familyWidth := calculateColumnWidthsTerminal() diff --git a/main.go b/main.go index 5b3790c..a74c84c 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "github.com/sammcj/gollama/config" "github.com/sammcj/gollama/logging" + "github.com/sammcj/gollama/utils" "github.com/sammcj/gollama/vramestimator" ) @@ -309,10 +310,10 @@ func main() { } if *ollamaDirFlag == "" { - app.ollamaModelsDir = filepath.Join(os.Getenv("HOME"), ".ollama", "models") + app.ollamaModelsDir = filepath.Join(utils.GetHomeDir(), ".ollama", "models") } if *lmStudioDirFlag == "" { - app.lmStudioModelsDir = filepath.Join(os.Getenv("HOME"), ".cache", "lm-studio", "models") + app.lmStudioModelsDir = filepath.Join(utils.GetHomeDir(), ".cache", "lm-studio", "models") } if *listFlag { @@ -345,13 +346,16 @@ func main() { for _, model := range models { // if cfg.LMStudioFilePaths is empty, use the default path in the user's home directory / .cache / lm-studio / models if cfg.LMStudioFilePaths == "" { - cfg.LMStudioFilePaths = filepath.Join(os.Getenv("HOME"), ".cache", "lm-studio", "models") + cfg.LMStudioFilePaths = filepath.Join(utils.GetHomeDir(), ".cache", "lm-studio", "models") } message, err := linkModel(model.Name, cfg.LMStudioFilePaths, false, client) logging.InfoLogger.Println(message) fmt.Printf("Linking model %s to %s\n", model.Name, cfg.LMStudioFilePaths) if err != nil { logging.ErrorLogger.Printf("Error linking model %s: %v\n", model.Name, err) + fmt.Println("Error: Linking models failed. Please check if you are running without Administrator on Windows.") + fmt.Printf("Error detail: %v\n", err) + os.Exit(1) } else { logging.InfoLogger.Printf("Model %s linked\n", model.Name) } diff --git a/operations.go b/operations.go index 32bc2ed..93082a3 100644 --- a/operations.go +++ b/operations.go @@ -19,6 +19,7 @@ import ( "github.com/ollama/ollama/api" "github.com/sammcj/gollama/config" "github.com/sammcj/gollama/logging" + "github.com/sammcj/gollama/utils" ) func runModel(model string, cfg *config.Config) tea.Cmd { @@ -584,7 +585,7 @@ func copyModelfile(modelName, newModelName string, client *api.Client) (string, output := []byte(resp.Modelfile) - err = os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".config", "gollama", "modelfiles"), os.ModePerm) + err = os.MkdirAll(filepath.Join(utils.GetHomeDir(), ".config", "gollama", "modelfiles"), os.ModePerm) if err != nil { logging.ErrorLogger.Printf("Error creating modelfiles directory: %v\n", err) return "", err @@ -594,7 +595,7 @@ func copyModelfile(modelName, newModelName string, client *api.Client) (string, newModelName = strings.ReplaceAll(newModelName, "/", "-") newModelName = strings.ReplaceAll(newModelName, ":", "-") - newModelfilePath := filepath.Join(os.Getenv("HOME"), ".config", "gollama", "modelfiles", newModelName+".modelfile") + newModelfilePath := filepath.Join(utils.GetHomeDir(), ".config", "gollama", "modelfiles", newModelName+".modelfile") err = os.WriteFile(newModelfilePath, output, 0644) if err != nil { diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..c303079 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,28 @@ +package utils + +import ( + "os" + "path/filepath" + + "github.com/sammcj/gollama/logging" +) + +func GetHomeDir() string { + homeDir, err := os.UserHomeDir() + if err != nil { + logging.ErrorLogger.Printf("Failed to get user home directory: %v\n", err) + + return "" + } + return homeDir +} + +// getConfigDir returns the directory of the configuration JSON file. +func GetConfigDir() string { + return filepath.Join(GetHomeDir(), ".config", "gollama") +} + +// getConfigPath returns the path to the configuration JSON file. +func GetConfigPath() string { + return filepath.Join(GetHomeDir(), ".config", "gollama", "config.json") +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000..7803957 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,46 @@ +package utils + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestGetHomeDir(t *testing.T) { + expected := homeDir() + got := GetHomeDir() + if got != expected { + t.Errorf("GetHomeDir() = %v, want %v", got, expected) + } +} + +func TestGetConfigDir(t *testing.T) { + expected := filepath.Join(homeDir(), ".config", "gollama") + got := GetConfigDir() + if got != expected { + t.Errorf("GetConfigDir() = %v, want %v", got, expected) + } +} + +func TestGetConfigPath(t *testing.T) { + expected := filepath.Join(homeDir(), ".config", "gollama", "config.json") + got := GetConfigPath() + if got != expected { + t.Errorf("GetConfigPath() = %v, want %v", got, expected) + } +} + +func homeDir() string { + // Get User Home directory (simplified). Refer to "os/file" + var env string + if runtime.GOOS == "windows" { + env = "USERPROFILE" + } else { + env = "HOME" + } + if v := os.Getenv(env); v != "" { + return v + } + return "" +} diff --git a/vramestimator/vramestimator.go b/vramestimator/vramestimator.go index 4436758..b71c82e 100644 --- a/vramestimator/vramestimator.go +++ b/vramestimator/vramestimator.go @@ -20,6 +20,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/olekukonko/tablewriter" "github.com/sammcj/gollama/logging" + "github.com/sammcj/gollama/utils" "github.com/shirou/gopsutil/v3/mem" ) @@ -375,7 +376,7 @@ func GetHuggingFaceToken() string { accessToken = os.Getenv("HF_TOKEN") } if accessToken == "" { - tokenPath := filepath.Join(os.Getenv("HOME"), ".huggingface/token") + tokenPath := filepath.Join(utils.GetHomeDir(), ".huggingface/token") if _, err := os.Stat(tokenPath); err == nil { token, err := os.ReadFile(tokenPath) if err == nil { @@ -395,7 +396,7 @@ func GetModelConfig(modelID string) (ModelConfig, error) { } cacheMutex.RUnlock() - baseDir := filepath.Join(os.Getenv("HOME"), ".cache/huggingface/hub", modelID) + baseDir := filepath.Join(utils.GetHomeDir(), ".cache/huggingface/hub", modelID) configPath := filepath.Join(baseDir, "config.json") indexPath := filepath.Join(baseDir, "model.safetensors.index.json")