refactor(root): seperate into small files

This commit is contained in:
verse91
2026-04-16 00:05:49 +07:00
parent 887b5af958
commit db8b6ec91b
4 changed files with 490 additions and 457 deletions
+2 -457
View File
@@ -1,18 +1,9 @@
package root
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"github.com/creack/pty"
"github.com/spf13/cobra"
"github.com/versenilvis/iris/commands/core"
_ "github.com/versenilvis/iris/commands/dev"
@@ -21,9 +12,6 @@ import (
_ "github.com/versenilvis/iris/commands/runner"
_ "github.com/versenilvis/iris/commands/search"
_ "github.com/versenilvis/iris/commands/view"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/integration/shell"
"golang.org/x/term"
)
var (
@@ -36,8 +24,8 @@ It works exactly like coding editor suggestion menu drop down.`,
if debugMode {
f, _ := os.OpenFile("iris.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
debugLogger = f
core.DebugWriter = f // Link core logger
fmt.Fprintf(debugLogger, "--- IRIS Debug Started ---\n")
core.DebugWriter = f
fmt.Fprintf(debugLogger, "--- IRIS DEBUG LOG ---\n")
}
runWrapper()
},
@@ -71,446 +59,3 @@ func Execute() {
os.Exit(1)
}
}
func detectShell() string {
pid := os.Getppid()
for i := 0; i < 5 && pid > 1; i++ {
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err == nil {
comm := strings.ToLower(strings.TrimSpace(string(data)))
if strings.Contains(comm, "zsh") {
return "zsh"
}
if strings.Contains(comm, "bash") {
return "bash"
}
if strings.Contains(comm, "fish") {
return "fish"
}
}
data, err = os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
if err != nil {
break
}
fields := strings.Fields(string(data))
if len(fields) > 3 {
ppid, _ := strconv.Atoi(fields[3])
if ppid == pid || ppid <= 1 {
break
}
pid = ppid
} else {
break
}
}
s := os.Getenv("SHELL")
if strings.Contains(s, "zsh") {
return "zsh"
}
return "bash"
}
type procInfo struct {
pid int
ppid int
comm string
}
func getActiveInnerShell(rootPid int, defaultShell string) string {
cmd := exec.Command("ps", "-e", "-o", "pid,ppid,comm")
out, err := cmd.Output()
if err != nil {
return defaultShell
}
lines := strings.Split(string(out), "\n")
childrenMap := make(map[int][]procInfo)
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) >= 3 && fields[0] != "PID" {
pid, _ := strconv.Atoi(fields[0])
ppid, _ := strconv.Atoi(fields[1])
comm := strings.ToLower(strings.Join(fields[2:], " "))
childrenMap[ppid] = append(childrenMap[ppid], procInfo{pid, ppid, comm})
}
}
var findDeepest func(pid int, current string) string
findDeepest = func(pid int, current string) string {
shell := current
for _, child := range childrenMap[pid] {
childShell := shell
if strings.Contains(child.comm, "zsh") {
childShell = "zsh"
}
if strings.Contains(child.comm, "bash") {
childShell = "bash"
}
if strings.Contains(child.comm, "fish") {
childShell = "fish"
}
if deepest := findDeepest(child.pid, childShell); deepest != "" {
shell = deepest
}
}
return shell
}
return findDeepest(rootPid, defaultShell)
}
func runWrapper() {
r, w, err := os.Pipe()
if err != nil {
return
}
var shellName string
if active := os.Getenv("IRIS_ACTIVE_SHELL"); active != "" {
shellName = active
os.Unsetenv("IRIS_ACTIVE_SHELL")
} else if shellFlag != "" {
shellName = shellFlag
} else {
shellName = detectShell()
}
shell.Init(shellName)
adapter := shell.Current
c := exec.Command(adapter.GetShellPath())
c.ExtraFiles = make([]*os.File, 11)
c.ExtraFiles[10] = w
c.Env = adapter.GetEnv(10, os.Getpid())
ptmx, err := pty.Start(c)
if err != nil {
fmt.Fprintf(os.Stderr, "[IRIS] failed to start PTY: %v\n", err)
return
}
defer ptmx.Close()
_ = pty.InheritSize(os.Stdin, ptmx)
core.ShellPID = c.Process.Pid
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }()
sigCh := make(chan os.Signal, 2)
signal.Notify(sigCh, syscall.SIGWINCH, syscall.SIGUSR1)
go func() {
for s := range sigCh {
switch s {
case syscall.SIGWINCH:
_ = pty.InheritSize(os.Stdin, ptmx)
case syscall.SIGUSR1:
exe, _ := os.Executable()
os.Setenv("IRIS_RELOADED", "true")
// capture the current shell state
innerShell := getActiveInnerShell(c.Process.Pid, shellName)
if innerShell != "" {
os.Setenv("IRIS_ACTIVE_SHELL", innerShell)
}
// kill the child process before reloading
if c.Process != nil {
_ = syscall.Kill(c.Process.Pid, syscall.SIGKILL)
ptmx.Close()
}
if oldState != nil {
_ = term.Restore(int(os.Stdin.Fd()), oldState)
}
_ = syscall.Exec(exe, os.Args, os.Environ())
}
}
}()
overlay := integration.NewOverlay()
// pty -> stdout
go func() {
buf := make([]byte, 4096)
for {
n, err := ptmx.Read(buf)
if err != nil {
if err == io.EOF {
_ = term.Restore(int(os.Stdin.Fd()), oldState)
os.Exit(0)
}
continue
}
os.Stdout.Write(buf[:n])
}
}()
// ipc pipe
go func() {
scanner := bufio.NewScanner(r)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\x00'); i >= 0 {
return i + 1, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
})
for scanner.Scan() {
query := scanner.Text()
results := mergeResults(query, "spec")
if len(results) == 0 {
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
continue
}
os.Stdout.Write([]byte(overlay.Clear()))
overlay.UpdateItems(results)
os.Stdout.Write([]byte(overlay.Render()))
}
}()
var naiveBuffer string
suggestionsEnabled := true
mode := "spec"
renderOverlay := func() {
if !suggestionsEnabled {
return
}
// If buffer is empty (e.g. backspace to 0 or after reload), clear overlay immediately
if naiveBuffer == "" {
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
return
}
debugLog("[Render] query: '%s', mode: %s", naiveBuffer, mode)
results := mergeResults(naiveBuffer, mode)
debugLog("[Render] results found: %d", len(results))
if len(results) == 0 {
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
} else {
var buf strings.Builder
if overlay.Visible {
buf.WriteString(overlay.Clear())
}
overlay.UpdateItems(results)
buf.WriteString(overlay.Render())
os.Stdout.Write([]byte(buf.String()))
}
}
// Trigger initial render to clear any potential artifacts from previous session
renderOverlay()
for {
inputSlice := make([]byte, 128)
n, err := os.Stdin.Read(inputSlice)
if err != nil {
break
}
if n > 0 {
shouldOverlayDraw := false
for i := 0; i < n; i++ {
b := inputSlice[i]
intercepted := false
// Detect Escape sequence (e.g. arrows, Shift+Tab) early
if b == '\033' {
// Shift+Tab: \033[Z
if i+2 < n && inputSlice[i+1] == '[' && inputSlice[i+2] == 'Z' {
intercepted = true
suggestionsEnabled = !suggestionsEnabled
if !suggestionsEnabled {
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
} else {
shouldOverlayDraw = true
}
i += 2
continue
}
ptmx.Write([]byte{b})
// arrow key monitoring inside escape
if i+2 < n && inputSlice[i+1] == '[' {
if overlay.Visible {
switch inputSlice[i+2] {
case 'A': // up
overlay.Cursor--
if overlay.Cursor < 0 {
overlay.Cursor = 0
}
os.Stdout.Write([]byte(overlay.Render()))
case 'B': // down
overlay.Cursor++
if overlay.Cursor >= len(overlay.Items) {
overlay.Cursor = len(overlay.Items) - 1
}
os.Stdout.Write([]byte(overlay.Render()))
}
}
}
// skip sequence
for j := i + 1; j < n; j++ {
char := inputSlice[j]
ptmx.Write([]byte{char})
i = j
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char == '~' {
break
}
}
continue
}
// Iris shortcuts
if b == 0x12 { // ctrl+r
intercepted = true
if mode == "spec" {
mode = "history"
} else {
mode = "spec"
}
shouldOverlayDraw = true
} else if b == 0x1b && n == 1 { // Standalone ESC (Dismiss CURRENT menu only)
intercepted = true
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
shouldOverlayDraw = false // Just hide it for this render
continue
} else if overlay.Visible && (b == 0x0d || b == 0x0a) { // Enter while menu open
intercepted = true
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
// Send Enter to PTY to execute current buffer as-is
ptmx.Write([]byte{0x0d})
naiveBuffer = ""
shouldOverlayDraw = false
continue
} else if b == 0x09 { // Tab (Universal Intercept - Select/Commit)
intercepted = true
if !overlay.Visible {
debugLog("[Input] Tab pressed: opening menu")
shouldOverlayDraw = true
} else {
// SELECT and COMMIT
selected := overlay.Items[overlay.Cursor].Cmd
debugLog("[Input] Tab pressed: committing '%s'", selected)
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
// Auto-add space for spec mode
if mode == "spec" {
selected = strings.TrimSpace(selected) + " "
}
naiveBuffer = selected
mode = "spec"
// Sync PTY with the new buffer (Ctrl+A then Ctrl+K to clear rest)
ptmx.Write([]byte{0x01, 0x0b}) // bash/zsh style home and clear
ptmx.Write([]byte(selected))
shouldOverlayDraw = false
}
continue
}
// normal typing
if !intercepted {
ptmx.Write([]byte{b})
switch b {
case 127, 0x08: // backspace
if len(naiveBuffer) > 0 {
naiveBuffer = naiveBuffer[:len(naiveBuffer)-1]
shouldOverlayDraw = true
}
case 0x17: // Ctrl+W: delete one word
trimBuf := strings.TrimRight(naiveBuffer, " ")
lastSpace := strings.LastIndex(trimBuf, " ")
if lastSpace >= 0 {
naiveBuffer = trimBuf[:lastSpace+1]
} else {
naiveBuffer = ""
}
shouldOverlayDraw = true
case '\r', 0x03, 0x15, 0x0C: // Enter, Ctrl+C, Ctrl+U, Ctrl+L
naiveBuffer = ""
mode = "spec"
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
default:
if b >= 32 && b <= 126 {
naiveBuffer += string(b)
shouldOverlayDraw = true
}
}
}
}
if shouldOverlayDraw {
renderOverlay()
}
}
}
}
func mergeResults(query string, mode string) []core.Suggestion {
if query == "" {
debugLog("[Merge] Query empty, returning nil")
return nil
}
normalizedQuery := strings.TrimSpace(query)
seen := make(map[string]bool)
deduped := []core.Suggestion{}
if mode == "history" {
histResults, _ := integration.SearchHistory(query)
for _, h := range histResults {
normalizedCmd := strings.TrimSpace(h.Cmd)
if seen[normalizedCmd] {
continue
}
seen[normalizedCmd] = true
deduped = append(deduped, core.Suggestion{
Cmd: h.Cmd,
Desc: " history",
Icon: fmt.Sprintf("%d", h.ID),
})
if len(deduped) >= 10 {
break
}
}
debugLog("[Merge] History mode found %d items", len(deduped))
return deduped
}
debugLog("[Merge] Calling Lookup for '%s'", query)
cmdResults := core.Lookup(query)
debugLog("[Merge] Lookup returned %d raw items", len(cmdResults))
for _, s := range cmdResults {
normalizedCmd := strings.TrimSpace(s.Cmd)
// CRITICAL: Filter out exact matches to prevent infinite tab loops
if normalizedCmd == normalizedQuery {
debugLog("[Merge] Filtered EXACT MATCH: '%s'", normalizedCmd)
continue
}
if !seen[s.Cmd] {
seen[s.Cmd] = true
deduped = append(deduped, s)
}
}
if len(deduped) > 10 {
deduped = deduped[:10]
}
return deduped
}