← back to jimbojd72__gopener

Function bodies 54 total

All specs Real LLM only Function bodies
main function · go · L13-L28 (16 LOC)
cmd/gopener/main.go
func main() {
	cfg, err := config.Load()
	if err != nil {
		fmt.Fprintf(os.Stderr, "gopener: failed to load config: %v\n", err)
		os.Exit(1)
	}

	l := launcher.New()
	app := tui.NewApp(cfg, l)

	p := tea.NewProgram(app, tea.WithAltScreen())
	if _, err := p.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "gopener: %v\n", err)
		os.Exit(1)
	}
}
configPath function · go · L29-L39 (11 LOC)
internal/config/config.go
func configPath() (string, error) {
	base := os.Getenv("XDG_CONFIG_HOME")
	if base == "" {
		var err error
		base, err = os.UserConfigDir()
		if err != nil {
			return "", err
		}
	}
	return filepath.Join(base, "gopener", "config.json"), nil
}
Load function · go · L41-L60 (20 LOC)
internal/config/config.go
func Load() (*Config, error) {
	path, err := configPath()
	if err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if os.IsNotExist(err) {
		return Default(), nil
	}
	if err != nil {
		return nil, err
	}

	var cfg Config
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, err
	}
	return &cfg, nil
}
Save method · go · L62-L77 (16 LOC)
internal/config/config.go
func (c *Config) Save() error {
	path, err := configPath()
	if err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		return err
	}

	data, err := json.MarshalIndent(c, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0644)
}
FindDir method · go · L80-L87 (8 LOC)
internal/config/config.go
func (c *Config) FindDir(path string) *DirConfig {
	for i := range c.Directories {
		if c.Directories[i].Path == path {
			return &c.Directories[i]
		}
	}
	return nil
}
FindProfile method · go · L90-L97 (8 LOC)
internal/config/config.go
func (c *Config) FindProfile(id string) *Profile {
	for i := range c.Profiles {
		if c.Profiles[i].ID == id {
			return &c.Profiles[i]
		}
	}
	return nil
}
Default function · go · L11-L22 (12 LOC)
internal/config/defaults.go
func Default() *Config {
	return &Config{
		SrcDir:   defaultSrcDir(),
		Terminal: DetectTerminal(),
		Profiles: []Profile{
			{ID: newID(), Label: "Claude", Cmd: "claude --continue"},
			{ID: newID(), Label: "Claude YOLO", Cmd: "claude --continue --dangerously-skip-permissions"},
			{ID: newID(), Label: "VS Code", Cmd: "code ."},
			{ID: newID(), Label: "IntelliJ", Cmd: "idea ."},
		},
	}
}
Repobility · severity-and-effort ranking · https://repobility.com
defaultSrcDir function · go · L24-L30 (7 LOC)
internal/config/defaults.go
func defaultSrcDir() string {
	home, err := os.UserHomeDir()
	if err != nil {
		return ""
	}
	return filepath.Join(home, "src")
}
newID function · go · L32-L36 (5 LOC)
internal/config/defaults.go
func newID() string {
	b := make([]byte, 8)
	_, _ = rand.Read(b)
	return fmt.Sprintf("%x", b)
}
DetectTerminal function · go · L13-L41 (29 LOC)
internal/config/terminal_darwin.go
func DetectTerminal() string {
	// First, try to detect the current terminal from the parent process
	if current := detectCurrentTerminal(); current != "" {
		return current
	}

	// List of terminal emulators to check, in order of preference
	terminals := []struct {
		name string
		path string
	}{
		{"Ghostty", "/Applications/Ghostty.app"},
		{"iTerm", "/Applications/iTerm.app"},
		{"Warp", "/Applications/Warp.app"},
		{"Kitty", "/Applications/kitty.app"},
		{"Alacritty", "/Applications/Alacritty.app"},
		{"Hyper", "/Applications/Hyper.app"},
		{"Terminal", "/System/Applications/Utilities/Terminal.app"},
	}

	for _, term := range terminals {
		if _, err := os.Stat(term.path); err == nil {
			return term.name
		}
	}

	// Fallback to default Terminal.app (always available on macOS)
	return "Terminal"
}
detectCurrentTerminal function · go · L44-L77 (34 LOC)
internal/config/terminal_darwin.go
func detectCurrentTerminal() string {
	// Check TERM_PROGRAM environment variable (set by many modern terminals)
	if termProgram := os.Getenv("TERM_PROGRAM"); termProgram != "" {
		switch termProgram {
		case "iTerm.app":
			return "iTerm"
		case "Apple_Terminal":
			return "Terminal"
		case "WarpTerminal":
			return "Warp"
		case "Hyper":
			return "Hyper"
		case "ghostty":
			return "Ghostty"
		}
	}

	// Check for Ghostty-specific environment variable
	if os.Getenv("GHOSTTY_RESOURCES_DIR") != "" {
		return "Ghostty"
	}

	// Check for Kitty-specific environment variable
	if os.Getenv("KITTY_WINDOW_ID") != "" {
		return "Kitty"
	}

	// Check for Alacritty-specific environment variable
	if os.Getenv("ALACRITTY_SOCKET") != "" || os.Getenv("ALACRITTY_LOG") != "" {
		return "Alacritty"
	}

	return ""
}
AvailableTerminals function · go · L80-L111 (32 LOC)
internal/config/terminal_darwin.go
func AvailableTerminals() []string {
	terminals := []struct {
		name string
		path string
	}{
		{"Terminal", "/System/Applications/Utilities/Terminal.app"},
		{"Ghostty", "/Applications/Ghostty.app"},
		{"iTerm", "/Applications/iTerm.app"},
		{"Warp", "/Applications/Warp.app"},
		{"Kitty", "/Applications/kitty.app"},
		{"Alacritty", "/Applications/Alacritty.app"},
		{"Hyper", "/Applications/Hyper.app"},
	}

	var available []string
	for _, term := range terminals {
		if _, err := os.Stat(term.path); err == nil {
			available = append(available, term.name)
		}
		// Also check in user's Applications folder
		userPath := filepath.Join(os.Getenv("HOME"), "Applications", filepath.Base(term.path))
		if _, err := os.Stat(userPath); err == nil && !contains(available, term.name) {
			available = append(available, term.name)
		}
	}

	if len(available) == 0 {
		available = append(available, "Terminal")
	}

	return available
}
contains function · go · L113-L120 (8 LOC)
internal/config/terminal_darwin.go
func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}
DetectTerminal function · go · L13-L58 (46 LOC)
internal/config/terminal_linux.go
func DetectTerminal() string {
	// First, check TERMINAL environment variable (often set by users)
	if term := os.Getenv("TERMINAL"); term != "" {
		if _, err := exec.LookPath(term); err == nil {
			return term
		}
	}

	// Try to detect from TERM_PROGRAM (set by some modern terminals)
	if termProgram := os.Getenv("TERM_PROGRAM"); termProgram != "" {
		if _, err := exec.LookPath(termProgram); err == nil {
			return termProgram
		}
	}

	// Check for terminal-specific environment variables
	if os.Getenv("KITTY_WINDOW_ID") != "" {
		return "kitty"
	}
	if os.Getenv("ALACRITTY_SOCKET") != "" || os.Getenv("ALACRITTY_LOG") != "" {
		return "alacritty"
	}
	if os.Getenv("WARP_USE_SSH_WRAPPER") != "" {
		return "warp-terminal"
	}

	// List of terminal emulators to check, in order of preference
	terminals := []string{
		"ghostty",
		"alacritty",
		"kitty",
		"warp-terminal",
		"gnome-terminal",
		"konsole",
		"xterm",
	}

	for _, term := range terminals {
		if _, err := exec.LookPath(term); err ==
AvailableTerminals function · go · L61-L88 (28 LOC)
internal/config/terminal_linux.go
func AvailableTerminals() []string {
	terminals := []string{
		"ghostty",
		"alacritty",
		"kitty",
		"warp-terminal",
		"gnome-terminal",
		"konsole",
		"xfce4-terminal",
		"mate-terminal",
		"xterm",
		"urxvt",
		"terminator",
	}

	var available []string
	for _, term := range terminals {
		if _, err := exec.LookPath(term); err == nil {
			available = append(available, term)
		}
	}

	if len(available) == 0 {
		available = append(available, "xterm")
	}

	return available
}
Repobility · code-quality intelligence platform · https://repobility.com
New function · go · L15-L17 (3 LOC)
internal/launcher/launcher_darwin.go
func New() Launcher {
	return &darwinLauncher{}
}
escapeAppleScript function · go · L20-L25 (6 LOC)
internal/launcher/launcher_darwin.go
func escapeAppleScript(s string) string {
	// Escape backslashes first, then double quotes
	s = strings.ReplaceAll(s, `\`, `\\`)
	s = strings.ReplaceAll(s, `"`, `\"`)
	return s
}
Launch method · go · L27-L101 (75 LOC)
internal/launcher/launcher_darwin.go
func (l *darwinLauncher) Launch(dirs []config.DirConfig, profiles []config.Profile, terminal string) error {
	// Default to Terminal.app if not specified
	if terminal == "" {
		terminal = "Terminal"
	}

	// Build profile map for quick lookup.
	profileMap := make(map[string]config.Profile, len(profiles))
	for _, p := range profiles {
		profileMap[p.ID] = p
	}

	for _, dir := range dirs {
		if !dir.Enabled {
			continue
		}
		for _, pid := range dir.ProfileIDs {
			p, ok := profileMap[pid]
			if !ok {
				continue
			}
			// Escape the path and command for AppleScript
			escapedPath := escapeAppleScript(dir.Path)
			escapedCmd := escapeAppleScript(p.Cmd)

			var cmd *exec.Cmd
			switch terminal {
			case "Ghostty":
				// Ghostty on macOS needs to use 'open' command with -e flag
				// Use -a instead of -na to open in existing instance
				shellCmd := fmt.Sprintf("cd \"%s\" && %s", escapedPath, escapedCmd)
				cmd = exec.Command("open", "-a", "Ghostty.app", "--args", "-e", "sh", "-c", sh
New function · go · L15-L17 (3 LOC)
internal/launcher/launcher_linux.go
func New() Launcher {
	return &linuxLauncher{}
}
Launch method · go · L19-L54 (36 LOC)
internal/launcher/launcher_linux.go
func (l *linuxLauncher) Launch(dirs []config.DirConfig, profiles []config.Profile, terminal string) error {
	term := terminal
	if term == "" {
		term = detectTerminal()
	}
	if term == "" {
		return fmt.Errorf("no supported terminal emulator found")
	}

	// Build profile map for quick lookup.
	profileMap := make(map[string]config.Profile, len(profiles))
	for _, p := range profiles {
		profileMap[p.ID] = p
	}

	for _, dir := range dirs {
		if !dir.Enabled {
			continue
		}
		for _, pid := range dir.ProfileIDs {
			p, ok := profileMap[pid]
			if !ok {
				continue
			}
			shellCmd := fmt.Sprintf("cd %q && %s", dir.Path, p.Cmd)
			cmd := buildCmd(term, shellCmd)
			if cmd == nil {
				continue
			}
			if err := cmd.Start(); err != nil {
				return fmt.Errorf("launching %s for %s: %w", p.Label, dir.Name, err)
			}
		}
	}
	return nil
}
detectTerminal function · go · L56-L69 (14 LOC)
internal/launcher/launcher_linux.go
func detectTerminal() string {
	if t := os.Getenv("TERMINAL"); t != "" {
		if path, err := exec.LookPath(t); err == nil && path != "" {
			return t
		}
	}
	candidates := []string{"ghostty", "wezterm", "kitty", "alacritty", "konsole", "gnome-terminal", "xterm"}
	for _, c := range candidates {
		if _, err := exec.LookPath(c); err == nil {
			return c
		}
	}
	return ""
}
buildCmd function · go · L71-L90 (20 LOC)
internal/launcher/launcher_linux.go
func buildCmd(term, shellCmd string) *exec.Cmd {
	switch term {
	case "ghostty":
		return exec.Command("ghostty", "-e", "bash", "-c", shellCmd)
	case "wezterm":
		return exec.Command("wezterm", "start", "--", "bash", "-c", shellCmd)
	case "kitty":
		return exec.Command("kitty", "bash", "-c", shellCmd)
	case "alacritty":
		return exec.Command("alacritty", "-e", "bash", "-c", shellCmd)
	case "konsole":
		return exec.Command("konsole", "-e", "bash", "-c", shellCmd)
	case "gnome-terminal":
		return exec.Command("gnome-terminal", "--", "bash", "-c", shellCmd)
	case "xterm":
		return exec.Command("xterm", "-e", "bash", "-c", shellCmd)
	default:
		return exec.Command(term, "-e", "bash", "-c", shellCmd)
	}
}
Scan function · go · L14-L49 (36 LOC)
internal/scanner/scanner.go
func Scan(srcDir string, existing []config.DirConfig) ([]config.DirConfig, error) {
	entries, err := os.ReadDir(srcDir)
	if err != nil {
		return nil, err
	}

	// Index existing configs by path for quick lookup.
	byPath := make(map[string]config.DirConfig, len(existing))
	for _, d := range existing {
		byPath[d.Path] = d
	}

	var result []config.DirConfig
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		fullPath := filepath.Join(srcDir, e.Name())
		if d, ok := byPath[fullPath]; ok {
			result = append(result, d)
		} else {
			result = append(result, config.DirConfig{
				Path:       fullPath,
				Name:       e.Name(),
				Enabled:    false,
				ProfileIDs: nil,
			})
		}
	}

	sort.Slice(result, func(i, j int) bool {
		return result[i].Name < result[j].Name
	})

	return result, nil
}
All rows scored by the Repobility analyzer (https://repobility.com)
NewApp function · go · L33-L52 (20 LOC)
internal/tui/app.go
func NewApp(cfg *config.Config, l launcher.Launcher) *App {
	app := &App{cfg: cfg, launcher: l}

	if cfg.SrcDir == "" {
		app.screen = screenSetup
	} else {
		app.screen = screenMain
		// Scan on startup.
		if dirs, err := scanner.Scan(cfg.SrcDir, cfg.Directories); err == nil {
			cfg.Directories = dirs
			_ = cfg.Save()
		}
	}

	app.setup = setup.New(cfg)
	app.main = mainscreen.New(cfg, l)
	app.profiles = profiles.New(cfg)
	app.settings = settings.New(cfg)
	return app
}
Init method · go · L54-L66 (13 LOC)
internal/tui/app.go
func (a *App) Init() tea.Cmd {
	switch a.screen {
	case screenSetup:
		return a.setup.Init()
	case screenMain:
		return a.main.Init()
	case screenProfiles:
		return a.profiles.Init()
	case screenSettings:
		return a.settings.Init()
	}
	return nil
}
Update method · go · L68-L125 (58 LOC)
internal/tui/app.go
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch a.screen {
	case screenSetup:
		updated, cmd := a.setup.Update(msg)
		a.setup = updated
		// Check if setup is done.
		if _, ok := msg.(setup.DoneMsg); ok {
			done := msg.(setup.DoneMsg)
			a.cfg.SrcDir = done.SrcDir
			if dirs, err := scanner.Scan(a.cfg.SrcDir, a.cfg.Directories); err == nil {
				a.cfg.Directories = dirs
			}
			_ = a.cfg.Save()
			a.main = mainscreen.New(a.cfg, a.launcher)
			a.screen = screenMain
			return a, a.main.Init()
		}
		return a, cmd

	case screenMain:
		updated, cmd := a.main.Update(msg)
		a.main = updated
		if _, ok := msg.(mainscreen.GoProfilesMsg); ok {
			a.profiles = profiles.New(a.cfg)
			a.screen = screenProfiles
			return a, a.profiles.Init()
		}
		if _, ok := msg.(mainscreen.GoSettingsMsg); ok {
			a.settings = settings.New(a.cfg)
			a.screen = screenSettings
			return a, a.settings.Init()
		}
		return a, cmd

	case screenProfiles:
		updated, cmd := a.profiles.Update(msg)
		a.profi
View method · go · L127-L139 (13 LOC)
internal/tui/app.go
func (a *App) View() string {
	switch a.screen {
	case screenSetup:
		return a.setup.View()
	case screenMain:
		return a.main.View()
	case screenProfiles:
		return a.profiles.View()
	case screenSettings:
		return a.settings.View()
	}
	return ""
}
New function · go · L54-L65 (12 LOC)
internal/tui/screens/main/main.go
func New(cfg *config.Config, l launcher.Launcher) Model {
	ti := textinput.New()
	ti.Placeholder = "/home/user/src"
	ti.CharLimit = 256
	ti.Width = 50
	return Model{
		cfg:      cfg,
		launcher: l,
		srcInput: ti,
		height:   24,
	}
}
Update method · go · L69-L83 (15 LOC)
internal/tui/screens/main/main.go
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	if wm, ok := msg.(tea.WindowSizeMsg); ok {
		m.height = wm.Height
		m.clampScroll()
	}
	switch m.mode {
	case modeList:
		return m.updateList(msg)
	case modeAssign:
		return m.updateAssign(msg)
	case modeChangeSrc:
		return m.updateChangeSrc(msg)
	}
	return m, nil
}
updateList method · go · L85-L159 (75 LOC)
internal/tui/screens/main/main.go
func (m Model) updateList(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, keys.Main.Quit):
			return m, tea.Quit
		case key.Matches(msg, keys.Main.Up):
			if m.cursor > 0 {
				m.cursor--
				m.clampScroll()
			}
		case key.Matches(msg, keys.Main.Down):
			if m.cursor < len(m.cfg.Directories)-1 {
				m.cursor++
				m.clampScroll()
			}
		case key.Matches(msg, keys.Main.PageUp):
			m.cursor -= m.visibleRows()
			if m.cursor < 0 {
				m.cursor = 0
			}
			m.clampScroll()
		case key.Matches(msg, keys.Main.PageDown):
			m.cursor += m.visibleRows()
			if m.cursor > len(m.cfg.Directories)-1 {
				m.cursor = len(m.cfg.Directories) - 1
			}
			m.clampScroll()
		case key.Matches(msg, keys.Main.Toggle):
			if len(m.cfg.Directories) > 0 {
				m.cfg.Directories[m.cursor].Enabled = !m.cfg.Directories[m.cursor].Enabled
				_ = m.cfg.Save()
			}
		case key.Matches(msg, keys.Main.Assign):
			if len(m.cfg.Directories) > 0 {
				m.enterAss
visibleRows method · go · L162-L168 (7 LOC)
internal/tui/screens/main/main.go
func (m Model) visibleRows() int {
	rows := m.height - reservedLines
	if rows < 3 {
		rows = 3
	}
	return rows
}
Open data scored by Repobility · https://repobility.com
clampScroll method · go · L171-L189 (19 LOC)
internal/tui/screens/main/main.go
func (m *Model) clampScroll() {
	visible := m.visibleRows()
	if m.cursor < m.scrollOffset {
		m.scrollOffset = m.cursor
	}
	if m.cursor >= m.scrollOffset+visible {
		m.scrollOffset = m.cursor - visible + 1
	}
	maxOffset := len(m.cfg.Directories) - visible
	if maxOffset < 0 {
		maxOffset = 0
	}
	if m.scrollOffset > maxOffset {
		m.scrollOffset = maxOffset
	}
	if m.scrollOffset < 0 {
		m.scrollOffset = 0
	}
}
enterAssign method · go · L191-L201 (11 LOC)
internal/tui/screens/main/main.go
func (m *Model) enterAssign(dirIdx int) {
	m.mode = modeAssign
	m.assignDirIdx = dirIdx
	m.assignCursor = 0
	// snapshot current profile selections for this dir
	selected := make(map[string]bool)
	for _, pid := range m.cfg.Directories[dirIdx].ProfileIDs {
		selected[pid] = true
	}
	m.assignToggled = selected
}
updateAssign method · go · L203-L236 (34 LOC)
internal/tui/screens/main/main.go
func (m Model) updateAssign(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, keys.Assign.Back):
			m.mode = modeList
		case key.Matches(msg, keys.Assign.Up):
			if m.assignCursor > 0 {
				m.assignCursor--
			}
		case key.Matches(msg, keys.Assign.Down):
			if m.assignCursor < len(m.cfg.Profiles)-1 {
				m.assignCursor++
			}
		case key.Matches(msg, keys.Assign.Toggle):
			if len(m.cfg.Profiles) > 0 {
				pid := m.cfg.Profiles[m.assignCursor].ID
				m.assignToggled[pid] = !m.assignToggled[pid]
			}
		case key.Matches(msg, keys.Assign.Confirm):
			// Save selections back.
			var ids []string
			for _, p := range m.cfg.Profiles {
				if m.assignToggled[p.ID] {
					ids = append(ids, p.ID)
				}
			}
			m.cfg.Directories[m.assignDirIdx].ProfileIDs = ids
			_ = m.cfg.Save()
			m.mode = modeList
		}
	}
	return m, nil
}
updateChangeSrc method · go · L238-L268 (31 LOC)
internal/tui/screens/main/main.go
func (m Model) updateChangeSrc(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyEsc:
			m.srcInput.Blur()
			m.mode = modeList
			return m, nil
		case tea.KeyEnter:
			val := strings.TrimSpace(m.srcInput.Value())
			if val == "" {
				return m, nil
			}
			m.cfg.SrcDir = val
			dirs, err := scanner.Scan(val, m.cfg.Directories)
			if err != nil {
				m.statusMsg = fmt.Sprintf("scan error: %v", err)
			} else {
				m.cfg.Directories = dirs
				m.statusMsg = fmt.Sprintf("src changed, %d dirs", len(dirs))
			}
			_ = m.cfg.Save()
			m.srcInput.Blur()
			m.mode = modeList
			return m, nil
		}
	}
	var cmd tea.Cmd
	m.srcInput, cmd = m.srcInput.Update(msg)
	return m, cmd
}
View method · go · L270-L279 (10 LOC)
internal/tui/screens/main/main.go
func (m Model) View() string {
	switch m.mode {
	case modeAssign:
		return m.viewAssign()
	case modeChangeSrc:
		return m.viewChangeSrc()
	default:
		return m.viewList()
	}
}
viewList method · go · L281-L337 (57 LOC)
internal/tui/screens/main/main.go
func (m Model) viewList() string {
	title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")).Render("gopener")
	srcLine := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render("src: " + m.cfg.SrcDir)

	var sb strings.Builder
	sb.WriteString(title + "  " + srcLine + "\n\n")

	if len(m.cfg.Directories) == 0 {
		sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("  (no directories — press r to scan)") + "\n")
	}

	visible := m.visibleRows()
	start := m.scrollOffset
	end := start + visible
	if end > len(m.cfg.Directories) {
		end = len(m.cfg.Directories)
	}

	for i := start; i < end; i++ {
		d := m.cfg.Directories[i]
		check := "[ ]"
		if d.Enabled {
			check = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("[x]")
		}

		// Collect profile labels for this dir.
		var labels []string
		for _, pid := range d.ProfileIDs {
			if p := m.cfg.FindProfile(pid); p != nil {
				labels = append(labels, p.Label)
			}
		}
		profilesStr := ""
viewChangeSrc method · go · L339-L343 (5 LOC)
internal/tui/screens/main/main.go
func (m Model) viewChangeSrc() string {
	title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")).Render("Change source directory")
	help := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render("  enter confirm  esc cancel")
	return strings.Join([]string{title, "", "  " + m.srcInput.View(), "", help}, "\n")
}
viewAssign method · go · L345-L380 (36 LOC)
internal/tui/screens/main/main.go
func (m Model) viewAssign() string {
	if m.assignDirIdx >= len(m.cfg.Directories) {
		return ""
	}
	dir := m.cfg.Directories[m.assignDirIdx]
	title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")).Render(
		fmt.Sprintf("Assign profiles → %s", dir.Name),
	)

	var sb strings.Builder
	sb.WriteString(title + "\n\n")

	if len(m.cfg.Profiles) == 0 {
		sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("  (no profiles — press esc, then p to add)") + "\n")
	}

	for i, p := range m.cfg.Profiles {
		check := "[ ]"
		if m.assignToggled[p.ID] {
			check = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("[x]")
		}
		cursor := "  "
		label := p.Label
		if i == m.assignCursor {
			cursor = "▸ "
			label = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true).Render(p.Label)
		}
		sb.WriteString(fmt.Sprintf("%s%s %s  %s\n", cursor, check, label, p.Cmd))
	}

	help := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render(
		"\n  s
Repobility · severity-and-effort ranking · https://repobility.com
New function · go · L41-L53 (13 LOC)
internal/tui/screens/profiles/profiles.go
func New(cfg *config.Config) Model {
	label := textinput.New()
	label.Placeholder = "label"
	label.CharLimit = 64
	label.Width = 30

	cmd := textinput.New()
	cmd.Placeholder = "command"
	cmd.CharLimit = 256
	cmd.Width = 50

	return Model{cfg: cfg, labelIn: label, cmdIn: cmd}
}
Update method · go · L57-L65 (9 LOC)
internal/tui/screens/profiles/profiles.go
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch m.mode {
	case modeList:
		return m.updateList(msg)
	case modeAdd, modeEdit:
		return m.updateEdit(msg)
	}
	return m, nil
}
updateList method · go · L67-L117 (51 LOC)
internal/tui/screens/profiles/profiles.go
func (m Model) updateList(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, keys.Profile.Back):
			return m, func() tea.Msg { return BackMsg{} }
		case key.Matches(msg, keys.Profile.Up):
			if m.cursor > 0 {
				m.cursor--
			}
		case key.Matches(msg, keys.Profile.Down):
			if m.cursor < len(m.cfg.Profiles)-1 {
				m.cursor++
			}
		case key.Matches(msg, keys.Profile.Add):
			m.mode = modeAdd
			m.labelIn.SetValue("")
			m.cmdIn.SetValue("")
			m.focused = 0
			m.labelIn.Focus()
			m.cmdIn.Blur()
			m.err = ""
			return m, textinput.Blink
		case key.Matches(msg, keys.Profile.Edit):
			if len(m.cfg.Profiles) == 0 {
				return m, nil
			}
			m.mode = modeEdit
			m.editIdx = m.cursor
			p := m.cfg.Profiles[m.cursor]
			m.labelIn.SetValue(p.Label)
			m.cmdIn.SetValue(p.Cmd)
			m.focused = 0
			m.labelIn.Focus()
			m.cmdIn.Blur()
			m.err = ""
			return m, textinput.Blink
		case key.Matches(msg, keys.Profile.Delete):
			if len(m
updateEdit method · go · L119-L166 (48 LOC)
internal/tui/screens/profiles/profiles.go
func (m Model) updateEdit(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.Type {
		case tea.KeyEsc:
			m.mode = modeList
			return m, nil
		case tea.KeyTab, tea.KeyShiftTab:
			m.focused = 1 - m.focused
			if m.focused == 0 {
				m.labelIn.Focus()
				m.cmdIn.Blur()
			} else {
				m.labelIn.Blur()
				m.cmdIn.Focus()
			}
			return m, textinput.Blink
		case tea.KeyEnter:
			label := strings.TrimSpace(m.labelIn.Value())
			cmd := strings.TrimSpace(m.cmdIn.Value())
			if label == "" || cmd == "" {
				m.err = "label and command are required"
				return m, nil
			}
			if m.mode == modeAdd {
				m.cfg.Profiles = append(m.cfg.Profiles, config.Profile{
					ID:    newID(),
					Label: label,
					Cmd:   cmd,
				})
			} else {
				m.cfg.Profiles[m.editIdx].Label = label
				m.cfg.Profiles[m.editIdx].Cmd = cmd
			}
			m.mode = modeList
			m.err = ""
			_ = m.cfg.Save()
			return m, func() tea.Msg { return SavedMsg{} }
		}
	}
	var cmds []tea.Cmd
	var
View method · go · L168-L176 (9 LOC)
internal/tui/screens/profiles/profiles.go
func (m Model) View() string {
	switch m.mode {
	case modeList:
		return m.viewList()
	case modeAdd, modeEdit:
		return m.viewEdit()
	}
	return ""
}
viewList method · go · L178-L204 (27 LOC)
internal/tui/screens/profiles/profiles.go
func (m Model) viewList() string {
	title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")).Render("Profiles")
	var sb strings.Builder
	sb.WriteString(title + "\n\n")

	if len(m.cfg.Profiles) == 0 {
		sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("  (no profiles)") + "\n")
	}
	for i, p := range m.cfg.Profiles {
		cursor := "  "
		line := fmt.Sprintf("%s  %-20s  %s", cursor, p.Label, p.Cmd)
		if i == m.cursor {
			cursor = "▸ "
			line = fmt.Sprintf("%s  %-20s  %s", cursor, p.Label, p.Cmd)
			line = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true).Render(line)
		} else {
			line = lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Render(line)
		}
		sb.WriteString(line + "\n")
	}

	help := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render(
		"\n  a add  e edit  d delete  esc back",
	)
	sb.WriteString(help)
	return sb.String()
}
viewEdit method · go · L206-L227 (22 LOC)
internal/tui/screens/profiles/profiles.go
func (m Model) viewEdit() string {
	heading := "Add Profile"
	if m.mode == modeEdit {
		heading = "Edit Profile"
	}
	title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")).Render(heading)

	labelLabel := "Label:"
	cmdLabel := "Command:"
	if m.focused == 0 {
		labelLabel = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Render("Label:")
	} else {
		cmdLabel = lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Render("Command:")
	}

	parts := []string{title, "", labelLabel, "  " + m.labelIn.View(), "", cmdLabel, "  " + m.cmdIn.View()}
	if m.err != "" {
		parts = append(parts, "", lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("  "+m.err))
	}
	parts = append(parts, "", lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render("  tab switch field  enter save  esc cancel"))
	return strings.Join(parts, "\n")
}
newID function · go · L230-L234 (5 LOC)
internal/tui/screens/profiles/profiles.go
func newID() string {
	b := make([]byte, 8)
	_, _ = rand.Read(b)
	return fmt.Sprintf("%x", b)
}
Repobility · code-quality intelligence platform · https://repobility.com
New function · go · L24-L29 (6 LOC)
internal/tui/screens/settings/settings.go
func New(cfg *config.Config) Model {
	return Model{
		cfg:            cfg,
		availableTerms: config.AvailableTerminals(),
	}
}
Update method · go · L33-L59 (27 LOC)
internal/tui/screens/settings/settings.go
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, keys.Settings.Back):
			return m, func() tea.Msg { return GoBackMsg{} }
		case key.Matches(msg, keys.Settings.Up):
			if m.cursor > 0 {
				m.cursor--
			}
		case key.Matches(msg, keys.Settings.Down):
			if m.cursor < len(m.availableTerms)-1 {
				m.cursor++
			}
		case key.Matches(msg, keys.Settings.Select):
			if m.cursor < len(m.availableTerms) {
				m.cfg.Terminal = m.availableTerms[m.cursor]
				if err := m.cfg.Save(); err != nil {
					m.statusMsg = fmt.Sprintf("error saving: %v", err)
				} else {
					m.statusMsg = fmt.Sprintf("Terminal set to %s", m.cfg.Terminal)
				}
			}
		}
	}
	return m, nil
}
View method · go · L61-L114 (54 LOC)
internal/tui/screens/settings/settings.go
func (m Model) View() string {
	title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")).Render("Settings")
	subtitle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render("Terminal Emulator")

	var sb strings.Builder
	sb.WriteString(title + "\n")
	sb.WriteString(subtitle + "\n\n")

	if len(m.availableTerms) == 0 {
		sb.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("  (no terminals detected)") + "\n")
	}

	// Find current terminal index for highlighting
	currentIdx := -1
	for i, term := range m.availableTerms {
		if term == m.cfg.Terminal {
			currentIdx = i
			break
		}
	}

	for i, term := range m.availableTerms {
		cursor := "  "
		check := "   "
		termStr := term

		// Mark current selection
		if i == currentIdx {
			check = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("[✓]")
		} else {
			check = "[ ]"
		}

		// Highlight cursor position
		if i == m.cursor {
			cursor = "▸ "
			termStr = lipgloss.NewStyle().Foregr
page 1 / 2next ›