227 lines
7.4 KiB
Go
227 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
func main() {
|
|
if handled, err := handleCompletionCommand(os.Args[1:], os.Stdout); handled {
|
|
if err != nil {
|
|
exitf("%v", err)
|
|
}
|
|
return
|
|
}
|
|
defaults := defaultConfig()
|
|
defaultConfigPath, err := configPath()
|
|
if err != nil {
|
|
exitf("configuration: %v", err)
|
|
}
|
|
flag.Usage = func() {
|
|
writeCLIHelp(flag.CommandLine.Output(), defaults, defaultConfigPath)
|
|
}
|
|
if len(os.Args) == 2 && os.Args[1] == "help" {
|
|
flag.Usage()
|
|
return
|
|
}
|
|
var (
|
|
configFile = flag.String("config", defaultConfigPath, "TOML configuration file")
|
|
repo = flag.String("repo", "", "limit pull requests to an owner/repository")
|
|
poll = flag.Duration("poll", defaults.RefreshInterval.Duration, "base interval between GitHub refreshes")
|
|
showAll = flag.Bool("all", defaults.ShowAll, "show every open PR in --repo instead of only assigned PRs")
|
|
limit = flag.Int("limit", defaults.Limit, "maximum number of open pull requests to load")
|
|
endpoint = flag.String("endpoint", defaults.Endpoint, "GitHub GraphQL API endpoint")
|
|
theme = flag.String("theme", defaults.Theme, "built-in or custom color theme")
|
|
foldResolved = flag.Bool("fold-resolved", defaults.Display.FoldResolved, "start resolved review threads folded")
|
|
listWidth = flag.Int("thread-list-width", defaults.Display.ThreadListWidthPercent, "thread-list width as a percentage of the terminal")
|
|
dashboardMode = flag.String("dashboard-mode", defaults.Display.DashboardMode, "open the dashboard by hotkey or as an intermediate screen")
|
|
compactReviews = flag.Bool("compact-reviews", defaults.Display.CompactReviews, "aggregate repeated submitted-review entries")
|
|
pathScroll = flag.Bool("path-scroll", defaults.Paths.Scroll, "scroll file paths that do not fit")
|
|
pathScrollRate = flag.Duration("path-scroll-interval", defaults.Paths.ScrollInterval.Duration, "interval between file-path scroll steps")
|
|
cacheEnabled = flag.Bool("cache", defaults.Cache.Enabled, "enable the local read cache and offline fallback")
|
|
cacheMaxAge = flag.Duration("cache-max-age", defaults.Cache.MaxAge.Duration, "oldest cache entry accepted for offline fallback")
|
|
cacheDir = flag.String("cache-dir", defaults.Cache.Directory, "local read-cache directory")
|
|
editorMode = flag.String("editor-mode", defaults.Editing.Mode, "description editor mode")
|
|
)
|
|
flag.Parse()
|
|
if flag.NArg() != 0 {
|
|
exitf("unexpected argument %q; run 'diple --help' for usage", flag.Arg(0))
|
|
}
|
|
|
|
visited := map[string]bool{}
|
|
flag.Visit(func(item *flag.Flag) { visited[item.Name] = true })
|
|
config, err := loadConfig(
|
|
*configFile,
|
|
visited["config"] || os.Getenv("DIPLE_CONFIG") != "" ||
|
|
os.Getenv("GH_THREADS_CONFIG") != "",
|
|
)
|
|
if err != nil {
|
|
exitf("configuration: %v", err)
|
|
}
|
|
if visited["repo"] {
|
|
config.Repository = *repo
|
|
} else if envRepo := os.Getenv("GH_REPO"); envRepo != "" {
|
|
config.Repository = envRepo
|
|
}
|
|
if visited["poll"] {
|
|
config.RefreshInterval.Duration = *poll
|
|
}
|
|
if visited["all"] {
|
|
config.ShowAll = *showAll
|
|
}
|
|
if visited["limit"] {
|
|
config.Limit = *limit
|
|
}
|
|
if visited["endpoint"] {
|
|
config.Endpoint = *endpoint
|
|
}
|
|
if visited["theme"] {
|
|
config.Theme = *theme
|
|
}
|
|
if visited["fold-resolved"] {
|
|
config.Display.FoldResolved = *foldResolved
|
|
}
|
|
if visited["thread-list-width"] {
|
|
config.Display.ThreadListWidthPercent = *listWidth
|
|
}
|
|
if visited["dashboard-mode"] {
|
|
config.Display.DashboardMode = *dashboardMode
|
|
}
|
|
if visited["compact-reviews"] {
|
|
config.Display.CompactReviews = *compactReviews
|
|
}
|
|
if visited["path-scroll"] {
|
|
config.Paths.Scroll = *pathScroll
|
|
}
|
|
if visited["path-scroll-interval"] {
|
|
config.Paths.ScrollInterval.Duration = *pathScrollRate
|
|
}
|
|
if visited["cache"] {
|
|
config.Cache.Enabled = *cacheEnabled
|
|
}
|
|
if visited["cache-max-age"] {
|
|
config.Cache.MaxAge.Duration = *cacheMaxAge
|
|
}
|
|
if visited["cache-dir"] {
|
|
config.Cache.Directory = *cacheDir
|
|
}
|
|
if visited["editor-mode"] {
|
|
config.Editing.Mode = *editorMode
|
|
}
|
|
if err := validateConfig(config); err != nil {
|
|
exitf("configuration: %v", err)
|
|
}
|
|
|
|
var owner, name string
|
|
if config.Repository != "" {
|
|
parts := strings.Split(config.Repository, "/")
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
exitf("--repo must be owner/name")
|
|
}
|
|
owner, name = parts[0], parts[1]
|
|
}
|
|
if err := applyTheme(config.Theme, config.CustomTheme); err != nil {
|
|
exitf("configuration: %v", err)
|
|
}
|
|
token, err := resolveToken(config.Endpoint)
|
|
if err != nil {
|
|
exitf("authenticate: %v", err)
|
|
}
|
|
|
|
client := NewGitHubClient(config.Endpoint, token)
|
|
var service GitHubService = client
|
|
if config.Cache.Enabled {
|
|
cacheDir := config.Cache.Directory
|
|
if cacheDir == "" {
|
|
cacheDir, err = defaultCacheDir()
|
|
if err != nil {
|
|
exitf("configuration: %v", err)
|
|
}
|
|
}
|
|
service = NewCachedGitHubService(
|
|
client, cacheDir, config.Cache.MaxAge.Duration, config.Cache.MaxEntries,
|
|
)
|
|
}
|
|
statePath := filepath.Join(filepath.Dir(*configFile), "state.json")
|
|
draftPath := filepath.Join(filepath.Dir(*configFile), "drafts.json")
|
|
var aiController *AIController
|
|
var aiStore *AIStore
|
|
if config.AI.Enabled {
|
|
aiDir := config.AI.StoreDirectory
|
|
if aiDir == "" {
|
|
aiDir = filepath.Join(filepath.Dir(*configFile), "ai")
|
|
}
|
|
aiStore = NewAIStore(aiDir)
|
|
diffService, ok := service.(AIDiffService)
|
|
if !ok {
|
|
exitf("configuration: GitHub service cannot provide authenticated PR diffs")
|
|
}
|
|
workingDirectory, cwdErr := os.Getwd()
|
|
if cwdErr != nil {
|
|
exitf("configuration: determine working directory: %v", cwdErr)
|
|
}
|
|
aiController = &AIController{
|
|
config: config.AI,
|
|
provider: NewCodexCLIProvider(config.AI, workingDirectory),
|
|
diffs: diffService,
|
|
store: aiStore,
|
|
}
|
|
}
|
|
app := NewAppWithSettings(
|
|
service, owner, name, config.ShowAll, config.Limit, config.RefreshInterval.Duration,
|
|
AppSettings{
|
|
FoldResolved: config.Display.FoldResolved,
|
|
ThreadListWidthPercent: config.Display.ThreadListWidthPercent,
|
|
DashboardMode: config.Display.DashboardMode,
|
|
CompactReviews: config.Display.CompactReviews,
|
|
ReadState: loadReadState(statePath),
|
|
Drafts: loadDraftStore(draftPath),
|
|
PathScroll: config.Paths.Scroll,
|
|
PathScrollInterval: config.Paths.ScrollInterval.Duration,
|
|
ThreadStatusOrder: config.Threads.StatusOrder,
|
|
ThreadWithinStatus: config.Threads.WithinStatus,
|
|
EditorMode: config.Editing.Mode,
|
|
KeyBindings: config.KeyBindings,
|
|
AI: aiController,
|
|
AIStore: aiStore,
|
|
},
|
|
)
|
|
cursorOutput := newTerminalCursorOutput(os.Stdout)
|
|
app.cursorOutput = cursorOutput
|
|
if _, err := tea.NewProgram(
|
|
app,
|
|
tea.WithAltScreen(),
|
|
tea.WithOutput(cursorOutput),
|
|
).Run(); err != nil {
|
|
exitf("run TUI: %v", err)
|
|
}
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func exitf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "diple: "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Keep interface drift visible at compile time.
|
|
var _ GitHubService = (*GitHubClient)(nil)
|
|
var _ GitHubMergeService = (*GitHubClient)(nil)
|
|
var _ GitHubMergeService = (*CachedGitHubService)(nil)
|
|
var _ GitHubWriteService = (*GitHubClient)(nil)
|
|
var _ GitHubWriteService = (*CachedGitHubService)(nil)
|
|
var _ GitHubPullRequestWriteService = (*GitHubClient)(nil)
|
|
var _ GitHubPullRequestWriteService = (*CachedGitHubService)(nil)
|