@@ -156,6 +156,7 @@ iris config show
|
|||||||
[core]
|
[core]
|
||||||
version = 1
|
version = 1
|
||||||
shell = "" # "zsh", "bash", "fish", or empty for auto-detection
|
shell = "" # "zsh", "bash", "fish", or empty for auto-detection
|
||||||
|
shell-login = false # run the selected shell as a login shell; can also be enabled with iris --shell-login
|
||||||
mode = "last" # "last", "spec", or "history"
|
mode = "last" # "last", "spec", or "history"
|
||||||
debug = false
|
debug = false
|
||||||
expand-alias = true
|
expand-alias = true
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func (d Duration) MarshalText() ([]byte, error) {
|
|||||||
type CoreConfig struct {
|
type CoreConfig struct {
|
||||||
Version int `toml:"version"`
|
Version int `toml:"version"`
|
||||||
Shell string `toml:"shell"`
|
Shell string `toml:"shell"`
|
||||||
|
ShellLogin bool `toml:"shell-login"`
|
||||||
Mode string `toml:"mode"`
|
Mode string `toml:"mode"`
|
||||||
Debug bool `toml:"debug"`
|
Debug bool `toml:"debug"`
|
||||||
ExpandAlias bool `toml:"expand-alias"`
|
ExpandAlias bool `toml:"expand-alias"`
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ func TestDefaultConfigAndState(t *testing.T) {
|
|||||||
if cfg.Core.Version != 1 {
|
if cfg.Core.Version != 1 {
|
||||||
t.Errorf("expected version 1, got %d", cfg.Core.Version)
|
t.Errorf("expected version 1, got %d", cfg.Core.Version)
|
||||||
}
|
}
|
||||||
|
if cfg.Core.ShellLogin {
|
||||||
|
t.Errorf("expected login shell to be disabled by default")
|
||||||
|
}
|
||||||
if cfg.UI.MaxSuggestions != 100 {
|
if cfg.UI.MaxSuggestions != 100 {
|
||||||
t.Errorf("expected suggestions 100, got %d", cfg.UI.MaxSuggestions)
|
t.Errorf("expected suggestions 100, got %d", cfg.UI.MaxSuggestions)
|
||||||
}
|
}
|
||||||
@@ -90,14 +93,23 @@ func TestValidationAndEnvironmentOverrides(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// UserConfigDir follows HOME on macOS and XDG_CONFIG_HOME on Unix.
|
||||||
|
// Override both so the test cannot read or write the user's real config.
|
||||||
|
t.Setenv("HOME", tmpDir)
|
||||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
|
|
||||||
configDir := filepath.Join(tmpDir, "iris")
|
configPath, err := ConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get config path: %v", err)
|
||||||
|
}
|
||||||
|
configDir := filepath.Dir(configPath)
|
||||||
if mkErr := os.MkdirAll(configDir, 0755); mkErr != nil {
|
if mkErr := os.MkdirAll(configDir, 0755); mkErr != nil {
|
||||||
t.Fatalf("failed to create config dir: %v", mkErr)
|
t.Fatalf("failed to create config dir: %v", mkErr)
|
||||||
}
|
}
|
||||||
configPath := filepath.Join(configDir, "config.toml")
|
|
||||||
tomlContent := `
|
tomlContent := `
|
||||||
|
[core]
|
||||||
|
shell-login = true
|
||||||
|
|
||||||
[ai]
|
[ai]
|
||||||
enabled = true
|
enabled = true
|
||||||
provider = "groq"
|
provider = "groq"
|
||||||
@@ -132,6 +144,9 @@ model = "qwen-2.5-coder-32b"
|
|||||||
if !cfg.Core.Debug {
|
if !cfg.Core.Debug {
|
||||||
t.Errorf("expected debug to be true")
|
t.Errorf("expected debug to be true")
|
||||||
}
|
}
|
||||||
|
if !cfg.Core.ShellLogin {
|
||||||
|
t.Errorf("expected login shell to be enabled from TOML")
|
||||||
|
}
|
||||||
if cfg.Core.Shell != "fish" {
|
if cfg.Core.Shell != "fish" {
|
||||||
t.Errorf("expected shell fish, got %q", cfg.Core.Shell)
|
t.Errorf("expected shell fish, got %q", cfg.Core.Shell)
|
||||||
}
|
}
|
||||||
@@ -178,6 +193,9 @@ func TestLoadSave(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// UserConfigDir follows HOME on macOS and XDG_CONFIG_HOME on Unix.
|
||||||
|
// Override both so the test cannot read or write the user's real config.
|
||||||
|
t.Setenv("HOME", tmpDir)
|
||||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
@@ -186,6 +204,7 @@ func TestLoadSave(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg.Core.Shell = "zsh"
|
cfg.Core.Shell = "zsh"
|
||||||
|
cfg.Core.ShellLogin = true
|
||||||
cfg.UI.MaxHeight = 20
|
cfg.UI.MaxHeight = 20
|
||||||
|
|
||||||
err = Save(cfg)
|
err = Save(cfg)
|
||||||
@@ -201,6 +220,9 @@ func TestLoadSave(t *testing.T) {
|
|||||||
if loaded.Core.Shell != "zsh" {
|
if loaded.Core.Shell != "zsh" {
|
||||||
t.Errorf("expected loaded shell to be zsh, got %q", loaded.Core.Shell)
|
t.Errorf("expected loaded shell to be zsh, got %q", loaded.Core.Shell)
|
||||||
}
|
}
|
||||||
|
if !loaded.Core.ShellLogin {
|
||||||
|
t.Errorf("expected loaded login shell setting to be true")
|
||||||
|
}
|
||||||
if loaded.UI.MaxHeight != 20 {
|
if loaded.UI.MaxHeight != 20 {
|
||||||
t.Errorf("expected loaded height to be 20, got %d", loaded.UI.MaxHeight)
|
t.Errorf("expected loaded height to be 20, got %d", loaded.UI.MaxHeight)
|
||||||
}
|
}
|
||||||
@@ -250,5 +272,3 @@ func TestMigration(t *testing.T) {
|
|||||||
t.Errorf("expected backup file update_state.json.bak to exist")
|
t.Errorf("expected backup file update_state.json.bak to exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ func DefaultConfig() *Config {
|
|||||||
Core: CoreConfig{
|
Core: CoreConfig{
|
||||||
Version: 1,
|
Version: 1,
|
||||||
Shell: "",
|
Shell: "",
|
||||||
|
ShellLogin: false,
|
||||||
Mode: "last",
|
Mode: "last",
|
||||||
Debug: false,
|
Debug: false,
|
||||||
ExpandAlias: true,
|
ExpandAlias: true,
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ version = 1
|
|||||||
# override shell: "bash", "zsh", "fish", keep empty for auto detection
|
# override shell: "bash", "zsh", "fish", keep empty for auto detection
|
||||||
shell = ""
|
shell = ""
|
||||||
|
|
||||||
|
# run the selected shell as a login shell
|
||||||
|
shell-login = false
|
||||||
|
|
||||||
# startup mode: "last", "spec", "history"
|
# startup mode: "last", "spec", "history"
|
||||||
# "last" = remember last mode used
|
# "last" = remember last mode used
|
||||||
mode = "last"
|
mode = "last"
|
||||||
|
|||||||
+18
-3
@@ -3,8 +3,10 @@ package root
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigCommands(t *testing.T) {
|
func TestConfigCommands(t *testing.T) {
|
||||||
@@ -14,14 +16,27 @@ func TestConfigCommands(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// UserConfigDir follows HOME on macOS and XDG_CONFIG_HOME on Unix.
|
||||||
|
// Override both so the command cannot read or write the user's real config.
|
||||||
|
t.Setenv("HOME", tmpDir)
|
||||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||||
|
|
||||||
ConfigInitCmd.Run(ConfigInitCmd, []string{})
|
ConfigInitCmd.Run(ConfigInitCmd, []string{})
|
||||||
|
|
||||||
configPath := filepath.Join(tmpDir, "iris", "config.toml")
|
configPath, err := config.ConfigPath()
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get config path: %v", err)
|
||||||
|
}
|
||||||
|
if _, statErr := os.Stat(configPath); statErr != nil {
|
||||||
t.Errorf("expected config file to be created at %s, but it was not", configPath)
|
t.Errorf("expected config file to be created at %s, but it was not", configPath)
|
||||||
}
|
}
|
||||||
|
content, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read config file: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(content), "shell-login = false") {
|
||||||
|
t.Error("expected initialized config to include shell-login = false")
|
||||||
|
}
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
ConfigShowCmd.SetOut(buf)
|
ConfigShowCmd.SetOut(buf)
|
||||||
|
|||||||
+4
-1
@@ -148,7 +148,7 @@ var setupCmd = &cobra.Command{
|
|||||||
|
|
||||||
var shellName string
|
var shellName string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
shellName = args[0]
|
shellName = filepath.Base(args[0])
|
||||||
} else {
|
} else {
|
||||||
shellPath := os.Getenv("SHELL")
|
shellPath := os.Getenv("SHELL")
|
||||||
shellName = filepath.Base(shellPath)
|
shellName = filepath.Base(shellPath)
|
||||||
@@ -208,6 +208,9 @@ version = 1
|
|||||||
# override shell: "bash", "zsh", "fish", keep empty for auto detection
|
# override shell: "bash", "zsh", "fish", keep empty for auto detection
|
||||||
shell = ""
|
shell = ""
|
||||||
|
|
||||||
|
# run the selected shell as a login shell
|
||||||
|
shell-login = false
|
||||||
|
|
||||||
# startup mode: "last", "spec", "history"
|
# startup mode: "last", "spec", "history"
|
||||||
# "last" = remember last mode used
|
# "last" = remember last mode used
|
||||||
mode = "last"
|
mode = "last"
|
||||||
|
|||||||
+8
-3
@@ -50,24 +50,29 @@ It works exactly like coding editor suggestion menu drop down.`,
|
|||||||
runWrapper()
|
runWrapper()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
shellFlag string
|
shellFlag string
|
||||||
debugMode bool
|
shellLoginFlag bool
|
||||||
|
debugMode bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&shellFlag, "shell", "s", "", "shell to use (bash, zsh, fish)")
|
rootCmd.PersistentFlags().StringVarP(&shellFlag, "shell", "s", "", "shell to use (bash, zsh, fish)")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&shellLoginFlag, "shell-login", false, "run the selected shell as a login shell")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&debugMode, "debug", "d", false, "enable debug logging to iris.log")
|
rootCmd.PersistentFlags().BoolVarP(&debugMode, "debug", "d", false, "enable debug logging to iris.log")
|
||||||
|
|
||||||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
if shellFlag != "" {
|
if shellFlag != "" {
|
||||||
config.Get().Core.Shell = shellFlag
|
config.Get().Core.Shell = shellFlag
|
||||||
}
|
}
|
||||||
|
if cmd.Flags().Changed("shell-login") {
|
||||||
|
config.Get().Core.ShellLogin = shellLoginFlag
|
||||||
|
}
|
||||||
logDir, err := config.CachePath()
|
logDir, err := config.CachePath()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Init(filepath.Join(logDir, "iris.log"), debugMode || config.Get().Core.Debug)
|
logger.Init(filepath.Join(logDir, "iris.log"), debugMode || config.Get().Core.Debug)
|
||||||
logger.Infof("IRIS session started: os=%s, arch=%s, go=%s, pid=%d", runtime.GOOS, runtime.GOARCH, runtime.Version(), os.Getpid())
|
logger.Infof("IRIS session started: os=%s, arch=%s, go=%s, pid=%d", runtime.GOOS, runtime.GOARCH, runtime.Version(), os.Getpid())
|
||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
logger.Debugf("IRIS loaded config: shell=%q, mode=%q, ghost-text=%v, max-suggestions=%d", cfg.Core.Shell, cfg.Core.Mode, cfg.UI.GhostText, cfg.UI.MaxSuggestions)
|
logger.Debugf("IRIS loaded config: shell=%q, shell-login=%v, mode=%q, ghost-text=%v, max-suggestions=%d", cfg.Core.Shell, cfg.Core.ShellLogin, cfg.Core.Mode, cfg.UI.GhostText, cfg.UI.MaxSuggestions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShellArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
login bool
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{name: "login", login: true, want: []string{"--login"}},
|
||||||
|
{name: "non-login", login: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := shellArgs(tt.login); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Fatalf("shellArgs(%v) = %v, want %v", tt.login, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellLoginFlagIsRegistered(t *testing.T) {
|
||||||
|
flag := rootCmd.PersistentFlags().Lookup("shell-login")
|
||||||
|
if flag == nil {
|
||||||
|
t.Fatal("expected --shell-login flag to be registered")
|
||||||
|
}
|
||||||
|
if flag.DefValue != "false" {
|
||||||
|
t.Fatalf("expected --shell-login to default to false, got %q", flag.DefValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-1
@@ -82,6 +82,13 @@ func saveMode(mode string) {
|
|||||||
_ = config.SaveState(state)
|
_ = config.SaveState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shellArgs(login bool) []string {
|
||||||
|
if login {
|
||||||
|
return []string{"--login"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
oldState *term.State
|
oldState *term.State
|
||||||
oldStateFd int
|
oldStateFd int
|
||||||
@@ -141,7 +148,7 @@ func runWrapper() {
|
|||||||
adapter := shell.Current
|
adapter := shell.Current
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
c := exec.CommandContext(ctx, adapter.GetShellPath())
|
c := exec.CommandContext(ctx, adapter.GetShellPath(), shellArgs(config.Get().Core.ShellLogin)...)
|
||||||
c.ExtraFiles = make([]*os.File, 11)
|
c.ExtraFiles = make([]*os.File, 11)
|
||||||
// pass write end of pipe to shell as fd 13 (since index 10 maps to 13)
|
// pass write end of pipe to shell as fd 13 (since index 10 maps to 13)
|
||||||
c.ExtraFiles[10] = w
|
c.ExtraFiles[10] = w
|
||||||
|
|||||||
Reference in New Issue
Block a user