package tui import ( "os" "os/exec" "regexp" "strings" "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var dangerousPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|/)`), regexp.MustCompile(`(?i)\bmkfs\b`), regexp.MustCompile(`(?i)\bdd\s+if=`), regexp.MustCompile(`(?i)\b(format\s+[A-Za-z]:)\b`), regexp.MustCompile(`(?i):\(\)\{.*\}`), regexp.MustCompile(`(?i)>(/dev/|/etc/|/boot/)`), regexp.MustCompile(`(?i)\bshutdown\b`), regexp.MustCompile(`(?i)\breboot\b`), regexp.MustCompile(`(?i)\bhalt\b`), regexp.MustCompile(`(?i)\bpoweroff\b`), } func isDangerousCommand(input string) bool { for _, pat := range dangerousPatterns { if pat.MatchString(input) { return true } } return false } func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": if m.termCmd != nil && m.termCmd.Process != nil { m.termCmd.Process.Kill() m.termRunning = false m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(errorColor).Render("^C")) m.termCmd = nil m.viewport.SetContent(m.renderContent()) return m, nil } now := time.Now() if m.ctrlCCount > 0 && now.Sub(m.lastCtrlC) < 2*time.Second { return m, tea.Quit } m.ctrlCCount++ m.lastCtrlC = now m.showingQuit = true m.confirmCursor = 1 m.viewport.SetContent(m.renderContent()) return m, nil case "ctrl+t": m.showingTabMenu = true m.tabMenuCursor = int(m.activeTab) return m, nil case "enter": if m.termRunning { return m, nil } input := strings.TrimSpace(m.termInput) m.termInput = "" if input == "" { return m, nil } if input == "exit" || input == "quit" { return m, nil } if input == "clear" { m.termLog = nil m.viewport.SetContent(m.renderContent()) return m, nil } if isDangerousCommand(input) { m.termLog = append(m.termLog, errMsgStyle.Render("blocked: potentially dangerous command")) m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() return m, nil } if strings.HasPrefix(input, "cd ") { dir := strings.TrimPrefix(input, "cd ") dir = strings.TrimSpace(dir) if dir == "~" { home, _ := os.UserHomeDir() dir = home } if err := os.Chdir(dir); err == nil { m.termCwd, _ = os.Getwd() m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ "+input)) } else { m.termLog = append(m.termLog, errMsgStyle.Render("cd: "+err.Error())) } m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() return m, nil } m.termLog = append(m.termLog, lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd+" $ ")+input) m.viewport.SetContent(m.renderContent()) m.viewport.GotoBottom() return m, m.runTermCommand(input) case "backspace": if len(m.termInput) > 0 { m.termInput = m.termInput[:len(m.termInput)-1] m.viewport.SetContent(m.renderContent()) } return m, nil default: if len(msg.String()) == 1 { m.termInput += msg.String() m.viewport.SetContent(m.renderContent()) } } return m, nil } func (m Model) runTermCommand(input string) tea.Cmd { return tea.Cmd(func() tea.Msg { shell := os.Getenv("SHELL") if shell == "" { shell = "/bin/sh" } cmd := exec.Command(shell, "-c", input) cmd.Dir = m.termCwd out, err := cmd.CombinedOutput() if err != nil { return termOutputMsg{line: string(out) + errMsgStyle.Render(err.Error())} } return termOutputMsg{line: string(out)} }) } func (m Model) renderTerminal() string { var b strings.Builder b.WriteString(sectionStyle.Render("Terminal")) b.WriteString(" ") b.WriteString(lipgloss.NewStyle().Foreground(dimColor).Render(m.termCwd)) b.WriteString("\n\n") for _, line := range m.termLog { b.WriteString(line + "\n") } return b.String() } func (m Model) renderTermInput() string { prompt := lipgloss.NewStyle().Foreground(successColor).Render("$ ") return prompt + m.termInput + lipgloss.NewStyle().Foreground(baseColor).Render("") }