Add configuration
This commit is contained in:
148
tui.go
148
tui.go
@@ -28,6 +28,7 @@ const (
|
||||
)
|
||||
|
||||
type tickMsg time.Time
|
||||
type pathTickMsg time.Time
|
||||
type prsLoadedMsg struct {
|
||||
prs []PullRequest
|
||||
err error
|
||||
@@ -66,23 +67,65 @@ type App struct {
|
||||
searchOrigin int
|
||||
helpVisible bool
|
||||
helpScroll int
|
||||
|
||||
foldResolved bool
|
||||
threadListWidthPercent int
|
||||
pathScroll bool
|
||||
pathScrollInterval time.Duration
|
||||
pathScrollStep int
|
||||
}
|
||||
|
||||
type AppSettings struct {
|
||||
FoldResolved bool
|
||||
ThreadListWidthPercent int
|
||||
PathScroll bool
|
||||
PathScrollInterval time.Duration
|
||||
}
|
||||
|
||||
func defaultAppSettings() AppSettings {
|
||||
return AppSettings{
|
||||
FoldResolved: true,
|
||||
ThreadListWidthPercent: 33,
|
||||
PathScrollInterval: 350 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApp(service GitHubService, owner, repo string, showAll bool, limit int, poll time.Duration) App {
|
||||
return NewAppWithSettings(service, owner, repo, showAll, limit, poll, defaultAppSettings())
|
||||
}
|
||||
|
||||
func NewAppWithSettings(
|
||||
service GitHubService,
|
||||
owner, repo string,
|
||||
showAll bool,
|
||||
limit int,
|
||||
poll time.Duration,
|
||||
settings AppSettings,
|
||||
) App {
|
||||
return App{
|
||||
service: service, owner: owner, repo: repo, showAll: showAll, limit: limit, poll: poll,
|
||||
folded: make(map[string]bool), loading: true,
|
||||
foldResolved: settings.FoldResolved, threadListWidthPercent: settings.ThreadListWidthPercent,
|
||||
pathScroll: settings.PathScroll, pathScrollInterval: settings.PathScrollInterval,
|
||||
}
|
||||
}
|
||||
|
||||
func (m App) Init() tea.Cmd {
|
||||
return tea.Batch(m.loadPRs(), m.nextTick())
|
||||
commands := []tea.Cmd{m.loadPRs(), m.nextTick()}
|
||||
if m.pathScroll {
|
||||
commands = append(commands, m.nextPathTick())
|
||||
}
|
||||
return tea.Batch(commands...)
|
||||
}
|
||||
|
||||
func (m App) nextTick() tea.Cmd {
|
||||
return tea.Tick(m.poll, func(t time.Time) tea.Msg { return tickMsg(t) })
|
||||
}
|
||||
|
||||
func (m App) nextPathTick() tea.Cmd {
|
||||
return tea.Tick(m.pathScrollInterval, func(t time.Time) tea.Msg { return pathTickMsg(t) })
|
||||
}
|
||||
|
||||
func (m App) loadPRs() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
@@ -117,6 +160,14 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(m.loadPRs(), m.nextTick())
|
||||
}
|
||||
return m, m.nextTick()
|
||||
case pathTickMsg:
|
||||
if m.pathScroll && m.screen == threadScreen {
|
||||
m.pathScrollStep++
|
||||
}
|
||||
if m.pathScroll {
|
||||
return m, m.nextPathTick()
|
||||
}
|
||||
return m, nil
|
||||
case prsLoadedMsg:
|
||||
m.loading = false
|
||||
if msg.err != nil {
|
||||
@@ -158,7 +209,7 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.scroll = 0
|
||||
}
|
||||
for _, thread := range m.details.Threads {
|
||||
if _, set := m.folded[thread.ID]; !set && thread.IsResolved {
|
||||
if _, set := m.folded[thread.ID]; !set && thread.IsResolved && m.foldResolved {
|
||||
m.folded[thread.ID] = true
|
||||
}
|
||||
}
|
||||
@@ -296,6 +347,7 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.details = PRDetails{PullRequest: m.prs[m.prIndex]}
|
||||
m.threadIndex, m.scroll, m.focus, m.listHidden, m.loading, m.err = 0, 0, threadListPane, false, true, nil
|
||||
m.searching, m.searchQuery = false, ""
|
||||
m.pathScrollStep = 0
|
||||
return m, m.loadDetails(m.details.PullRequest)
|
||||
}
|
||||
if m.screen == threadScreen {
|
||||
@@ -312,6 +364,7 @@ func (m App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.details = PRDetails{PullRequest: m.prs[m.prIndex]}
|
||||
m.threadIndex, m.scroll, m.focus, m.listHidden, m.loading, m.err = 0, 0, threadListPane, false, true, nil
|
||||
m.searching, m.searchQuery = false, ""
|
||||
m.pathScrollStep = 0
|
||||
return m, m.loadDetails(m.details.PullRequest)
|
||||
}
|
||||
if m.screen == threadScreen && len(m.details.Threads) > 0 {
|
||||
@@ -430,10 +483,18 @@ func (m App) detailPaneSize() (int, int) {
|
||||
if m.width < 70 || m.listHidden {
|
||||
return max(3, m.width), height
|
||||
}
|
||||
leftWidth := clamp(m.width/3, 30, 48)
|
||||
leftWidth := m.threadListWidth()
|
||||
return max(20, m.width-leftWidth-1), height
|
||||
}
|
||||
|
||||
func (m App) threadListWidth() int {
|
||||
percent := m.threadListWidthPercent
|
||||
if percent == 0 {
|
||||
percent = 33
|
||||
}
|
||||
return clamp(m.width*percent/100, 30, max(30, m.width-20))
|
||||
}
|
||||
|
||||
func (m App) detailViewportHeight() int {
|
||||
_, height := m.detailPaneSize()
|
||||
return max(1, height-2)
|
||||
@@ -533,18 +594,26 @@ func (m App) viewHelp() string {
|
||||
}
|
||||
|
||||
var (
|
||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F0B72F"))
|
||||
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
|
||||
activeStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")).Background(lipgloss.Color("#3B4261"))
|
||||
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#67C587"))
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#E5C07B"))
|
||||
badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#E06C75"))
|
||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#F0B72F"))
|
||||
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
|
||||
activeStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")).Background(lipgloss.Color("#3B4261"))
|
||||
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#67C587"))
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#E5C07B"))
|
||||
badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#E06C75"))
|
||||
paneInactiveColor = lipgloss.Color("#50566F")
|
||||
paneActiveColor = lipgloss.Color("#F0B72F")
|
||||
|
||||
selectedLineBackground = "\x1b[48;5;24m"
|
||||
suggestionRemoveBackground = "\x1b[48;5;52m"
|
||||
suggestionAddBackground = "\x1b[48;5;22m"
|
||||
changedRemoveBackground = "\x1b[48;2;55;0;0m"
|
||||
changedAddBackground = "\x1b[48;2;0;55;0m"
|
||||
)
|
||||
|
||||
func paneStyle(active bool) lipgloss.Style {
|
||||
color := lipgloss.Color("#50566F")
|
||||
color := paneInactiveColor
|
||||
if active {
|
||||
color = lipgloss.Color("#F0B72F")
|
||||
color = paneActiveColor
|
||||
}
|
||||
return lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(color)
|
||||
}
|
||||
@@ -636,7 +705,7 @@ func (m App) viewThreads() string {
|
||||
} else if m.listHidden {
|
||||
body = m.threadDetail(m.width, contentHeight)
|
||||
} else {
|
||||
leftWidth := clamp(m.width/3, 30, 48)
|
||||
leftWidth := m.threadListWidth()
|
||||
rightWidth := max(20, m.width-leftWidth-1)
|
||||
left := m.threadList(leftWidth, contentHeight)
|
||||
right := m.threadDetail(rightWidth, contentHeight)
|
||||
@@ -684,7 +753,11 @@ func (m App) threadList(width, height int) string {
|
||||
}
|
||||
suffix := fmt.Sprintf(":%d · %d", thread.Line, len(thread.Comments))
|
||||
pathWidth := max(4, innerWidth-lipgloss.Width(suffix)-2)
|
||||
line := icon + " " + pad(truncatePath(thread.Path, pathWidth), pathWidth) + suffix
|
||||
path := truncatePath(thread.Path, pathWidth)
|
||||
if m.pathScroll {
|
||||
path = scrollingPath(thread.Path, pathWidth, m.pathScrollStep)
|
||||
}
|
||||
line := icon + " " + pad(path, pathWidth) + suffix
|
||||
line = ansi.Truncate(line, innerWidth, "")
|
||||
if thread.IsResolved {
|
||||
line = dimStyle.Render(line)
|
||||
@@ -861,9 +934,9 @@ func highlightCodeRange(code string, changed codeRange, change byte) string {
|
||||
return code
|
||||
}
|
||||
|
||||
background := "\x1b[48;2;55;0;0m"
|
||||
background := changedRemoveBackground
|
||||
if change == '+' {
|
||||
background = "\x1b[48;2;0;55;0m"
|
||||
background = changedAddBackground
|
||||
}
|
||||
const reset = "\x1b[0m"
|
||||
before := ansi.Cut(code, 0, start)
|
||||
@@ -953,19 +1026,17 @@ func shortOID(oid string) string {
|
||||
}
|
||||
|
||||
func selectedBackground(line string, width int) string {
|
||||
const (
|
||||
background = "\x1b[48;5;24m"
|
||||
reset = "\x1b[0m"
|
||||
)
|
||||
const reset = "\x1b[0m"
|
||||
background := selectedLineBackground
|
||||
line = pad(ansi.Truncate(line, width, ""), width)
|
||||
line = strings.ReplaceAll(line, reset, reset+background)
|
||||
return background + line + reset
|
||||
}
|
||||
|
||||
func suggestionHighlight(gutter, code string, width int, change byte) string {
|
||||
background := "\x1b[48;5;52m"
|
||||
background := suggestionRemoveBackground
|
||||
if change == '+' {
|
||||
background = "\x1b[48;5;22m"
|
||||
background = suggestionAddBackground
|
||||
}
|
||||
const reset = "\x1b[0m"
|
||||
gutter = ansi.Truncate(gutter, width, "")
|
||||
@@ -1138,6 +1209,41 @@ func truncatePath(path string, width int) string {
|
||||
return "…" + ansi.Cut(path, pathWidth-width+1, pathWidth)
|
||||
}
|
||||
|
||||
func scrollingPath(path string, width, step int) string {
|
||||
const pauseSteps = 4
|
||||
if width <= 0 {
|
||||
return ""
|
||||
}
|
||||
pathWidth := ansi.StringWidth(path)
|
||||
if pathWidth <= width {
|
||||
return path
|
||||
}
|
||||
if width == 1 {
|
||||
return "…"
|
||||
}
|
||||
|
||||
visibleWidth := width - 1
|
||||
maxOffset := pathWidth - visibleWidth
|
||||
cycle := pauseSteps + maxOffset + pauseSteps
|
||||
phase := step % cycle
|
||||
offset := 0
|
||||
switch {
|
||||
case phase < pauseSteps:
|
||||
offset = 0
|
||||
case phase < pauseSteps+maxOffset:
|
||||
offset = phase - pauseSteps
|
||||
default:
|
||||
offset = maxOffset
|
||||
}
|
||||
if offset == 0 {
|
||||
return ansi.Cut(path, 0, visibleWidth) + "…"
|
||||
}
|
||||
if offset == maxOffset {
|
||||
return "…" + ansi.Cut(path, pathWidth-visibleWidth, pathWidth)
|
||||
}
|
||||
return "…" + ansi.Cut(path, offset, offset+width-2) + "…"
|
||||
}
|
||||
|
||||
func fuzzyPathScore(path, query string) (int, bool) {
|
||||
candidate := []rune(strings.ToLower(path))
|
||||
terms := strings.Fields(strings.ToLower(query))
|
||||
|
||||
Reference in New Issue
Block a user