diff --git a/cmd/app/main.go b/cmd/app/main.go index c49e729..d1423fa 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -5,51 +5,125 @@ import ( "os" "strings" - "nshell/executor" - "nshell/suggestions" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" - "github.com/c-bata/go-prompt" - "golang.org/x/term" + "nshell/executor" ) -var oldStateTerminal *term.State +// Сообщение, которое сигнализирует о завершении работы внешней команды +type processFinishedMsg struct{ err error } -func getColor(color string) prompt.Color { - color = strings.ToUpper(color) - - switch color { - case "CYAN": - return prompt.Cyan - case "RED": - return prompt.Red - case "FUCHSIA": - return prompt.Fuchsia - case "TURQUOISE": - return prompt.Turquoise - case "GREEN": - return prompt.Green - case "PURPLE": - return prompt.Purple - default: - return prompt.Green +type model struct { + textInput textinput.Model + lastInput string // Храним последнюю введенную строку для красивого лога + errText string // Храним ошибку, если команда не найдена +} + +func initialModel() model { + ti := textinput.New() + ti.Placeholder = "Type a command..." + ti.Focus() + ti.CharLimit = 250 + ti.SetWidth(60) + + // НАСТРОЙКА TRUECOLOR + prefixStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF00FA")). + Bold(true) + + ti.Prompt = prefixStyle.Render("noko> ") + //prefixStyle=lipgloss.NewStyle().Foreground(lipgloss.Color("#00FFD1")) + + return model{ + textInput: ti, + lastInput: "", + errText: "", } } +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc": + return m, tea.Quit + + case "enter": + inputVal := m.textInput.Value() + m.textInput.SetValue("") // Очищаем поле ввода сразу + m.errText = "" // Сбрасываем прошлые ошибки + + if strings.TrimSpace(inputVal) == "" { + return m, nil + } + + m.lastInput = inputVal + + // Передаем ввод в наш executor для подготовки команды + c, err := executor.PrepareCommand(inputVal) + if err != nil { + if err == executor.ErrBuiltInExit { + return m, tea.Quit + } + // Если exec.LookPath не нашел бинарник в системе + m.errText = fmt.Sprintf("noko-shell: command not found: %s", strings.Fields(inputVal)[0]) + return m, nil + } + + // Универсальный запуск процесса (и для ls, и для nano/top/htop) + // Мы связываем IO процесса с системным, чтобы stderr и stdout вывелись в терминал напрямую. + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + // Вызываем ExecProcess, который сворачивает TUI, выполняет команду и возвращает управление + return m, tea.ExecProcess(c, func(err error) tea.Msg { + return processFinishedMsg{err: err} + }) + } + + case processFinishedMsg: + if msg.err != nil { + // Ошибка самого процесса (например, неверные флаги у команды) + m.errText = fmt.Sprintf("process exited with error: %v", msg.err) + } + return m, nil + } + + var cmd tea.Cmd + m.textInput, cmd = m.textInput.Update(msg) + return m, cmd +} + +func (m model) View() tea.View { + var s strings.Builder + + if m.errText != "" { + errStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) + s.WriteString(errStyle.Render(m.errText)) + s.WriteString("\n") + m.errText = "" + } + + s.WriteString(m.textInput.View()) + + // Возвращаем объект tea.View, обернув в него строку + return tea.NewView(s.String()) +} + + func main() { - - fd := int(os.Stdin.Fd()) - if state, err := term.GetState(fd); err == nil { - oldStateTerminal = state + // Запускаем программу + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + fmt.Printf("Error running noko-shell: %v\n", err) + os.Exit(1) } - fmt.Println("Wellcome to noko-shell(nshell) :3") - - options := []prompt.Option{ - prompt.OptionLivePrefix(func() (string, bool) { - return executor.GetVariable("NSHELL_PREFIX_TEXT"), true - }), - prompt.OptionPrefixTextColor(getColor(executor.GetVariable("NSHELL_PREFIX_COLOR"))), - } - - p := prompt.New(executor.BuildExecutor(oldStateTerminal, &fd), suggestions.Completer, options...) - p.Run() } + diff --git a/executor/executor.go b/executor/executor.go index db902ea..a394912 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -2,69 +2,54 @@ package executor import ( - "fmt" - "os" + "errors" "os/exec" + "os" "strings" - - "golang.org/x/term" ) +var ErrBuiltInExit = errors.New("exit command") var localVars = make(map[string]string) -func BuildExecutor(oldStateTerminal *term.State, fd *int) func(string) { - return func(line string) { - line = strings.TrimSpace(line) - - if strings.Contains(line, "=") && !strings.HasPrefix(line, "export ") { - parts := strings.SplitN(line, "=", 2) +func PrepareCommand(in string) (*exec.Cmd, error) { + in = strings.TrimSpace(in) + if in == "" { + return nil, nil + } + if strings.Contains(in, "=") && !strings.HasPrefix(in, "export ") { + parts := strings.SplitN(in, "=", 2) key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) localVars[key] = value - return + return nil,nil } - line = os.Expand(line, GetVariable) + in = os.Expand(in, GetVariable) - if line == "" { - return - } - args := strings.Fields(line) - command := args[0] - commandArgs := args[1:] + parts := strings.Fields(in) + head := parts[0] + args := parts[1:] - if command == "exit" { - fmt.Println("Bye Bye my small deer :3") - if oldStateTerminal != nil { - _ = term.Restore(*fd, oldStateTerminal) - } - os.Exit(0) - } - - if command == "cd" { - if len(commandArgs) < 1 { - fmt.Fprintln(os.Stderr, "cd: argument was skipped") - return - } - if err := os.Chdir(commandArgs[0]); err != nil { - fmt.Fprintln(os.Stderr, err) - } - return - } - - cmd := exec.Command(command, commandArgs...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Execution error: %v\n", err) - } + switch head { + case "exit": + return nil, ErrBuiltInExit + case "help": + return exec.Command("echo", "noko-shell: available commands: any system command, help, exit"), nil } + + // Поиск системной утилиты в PATH (авто-проверка на существование) + fullPath, err := exec.LookPath(head) + if err != nil { + return nil, err + } + + // Возвращаем подготовленный процесс + return exec.Command(fullPath, args...), nil } + func GetVariable(key string) string { if val, exists := localVars[key]; exists { return val diff --git a/go.mod b/go.mod index 098b9ef..d5325dc 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,28 @@ module nshell -go 1.27.2 +go 1.26.8 require ( - github.com/c-bata/go-prompt v0.2.6 - golang.org/x/term v0.47.0 + charm.land/bubbles/v2 v2.2.1 + charm.land/bubbletea/v2 v2.1.0 + charm.land/lipgloss/v2 v2.0.6 ) require ( - github.com/mattn/go-colorable v0.1.7 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/mattn/go-tty v0.0.3 // indirect - github.com/pkg/term v1.2.0-beta.2 // indirect - golang.org/x/sys v0.49.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 // indirect + github.com/charmbracelet/x/ansi v0.11.9 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/mattn/go-runewidth v0.0.30 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v1.0.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect ) diff --git a/go.sum b/go.sum index f3b1511..e85827e 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,44 @@ -github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= -github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= -github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= -github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= -golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= -golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU= +charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc= +charm.land/bubbletea/v2 v2.1.0 h1:A9fj9h5LFrvQhQHFNggp8K8rvY6CUfgn9dMbb6p2HIw= +charm.land/bubbletea/v2 v2.1.0/go.mod h1:xWHeyxIX9/wiFU2kgPW04ojOZyTzrWf+8lGjUCaftiU= +charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= +charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 h1:hISLzUbwcvJbGoNR6A3L/TpguRRnACBmZOee8LcHLJw= +github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4/go.mod h1:aJDVk+WOVM1K04rdc/71PMPoUGfjxjhvXfP3Pjz1Zio= +github.com/charmbracelet/x/ansi v0.11.9 h1:kIrqLBxtTVbLyyHO2Evoi93EjQo1mylWmpNEsfPxXAM= +github.com/charmbracelet/x/ansi v0.11.9/go.mod h1:DG/hpOnaI2QJ/KMMqLL2h0KVhluuJLazf8973P+Vy5w= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= +github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= +github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= +github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/suggestions/suggestions.go b/suggestions/suggestions.go index 4e7b99b..94c4ecf 100644 --- a/suggestions/suggestions.go +++ b/suggestions/suggestions.go @@ -1,13 +1,17 @@ // Package suggestions package suggestions -import "github.com/c-bata/go-prompt" +import "strings" -var defaultSuggestions = []prompt.Suggest{ - {Text: "exit", Description: "Exit from shell"}, -} +var defaultSuggestions = []string{"exit"} -func Completer(d prompt.Document) []prompt.Suggest { - // Если пользователь уже начал что-то писать, фильтруем список - return prompt.FilterHasPrefix(defaultSuggestions, d.GetWordBeforeCursor(), true) +func Completer(in string) []string { + if in=="" {return nil} + var matched []string + for _,cmd := range defaultSuggestions { + if strings.HasPrefix(cmd,in){ + matched=append(matched, cmd) + } + } + return matched } diff --git a/tcolor/tcolor.go b/tcolor/tcolor.go deleted file mode 100644 index 2bb1005..0000000 --- a/tcolor/tcolor.go +++ /dev/null @@ -1,39 +0,0 @@ -// Package tcolor -package tcolor - -import ( - "strconv" - "regexp" - "fmt" -) - -type TColor struct{ - colorRe *regexp.Regexp -} - - -func (t *TColor) Init(){ - colorRe,colorReErr := regexp.Compile(`#[0-9a-fA-F]{6}`) - if colorReErr != nil{ - fmt.Errorf("Regexp color err: %s",colorReErr.Error()) - }else{ - t.colorRe = colorRe - } -} - -func (t *TColor) HexToANSI(hex string) string { - if t.colorRe.MatchString(hex) { - hex = hex[1:] - } - if len(hex) != 6 { - return "" // Возвращаем пустую строку при ошибке - } - r, _ := strconv.ParseUint(hex[0:2], 16, 8) - g, _ := strconv.ParseUint(hex[2:4], 16, 8) - b, _ := strconv.ParseUint(hex[4:6], 16, 8) - - // Формат TrueColor ESC[38;2;R;G;Bm - return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r, g, b) -} - -