kajji 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kajji +60 -0
- package/package.json +35 -55
- package/script/postinstall.mjs +50 -0
- package/LICENSE +0 -21
- package/README.md +0 -128
- package/bin/kajji.js +0 -2
- package/src/App.tsx +0 -229
- package/src/commander/bookmarks.ts +0 -129
- package/src/commander/diff.ts +0 -186
- package/src/commander/executor.ts +0 -285
- package/src/commander/files.ts +0 -87
- package/src/commander/log.ts +0 -99
- package/src/commander/operations.ts +0 -313
- package/src/commander/types.ts +0 -21
- package/src/components/AnsiText.tsx +0 -77
- package/src/components/BorderBox.tsx +0 -124
- package/src/components/FileTreeList.tsx +0 -105
- package/src/components/Layout.tsx +0 -48
- package/src/components/Panel.tsx +0 -143
- package/src/components/RevisionPicker.tsx +0 -165
- package/src/components/StatusBar.tsx +0 -158
- package/src/components/modals/BookmarkNameModal.tsx +0 -170
- package/src/components/modals/DescribeModal.tsx +0 -124
- package/src/components/modals/HelpModal.tsx +0 -372
- package/src/components/modals/RevisionPickerModal.tsx +0 -70
- package/src/components/modals/UndoModal.tsx +0 -75
- package/src/components/panels/BookmarksPanel.tsx +0 -768
- package/src/components/panels/CommandLogPanel.tsx +0 -40
- package/src/components/panels/LogPanel.tsx +0 -774
- package/src/components/panels/MainArea.tsx +0 -354
- package/src/context/command.tsx +0 -106
- package/src/context/commandlog.tsx +0 -45
- package/src/context/dialog.tsx +0 -217
- package/src/context/focus.tsx +0 -63
- package/src/context/helper.tsx +0 -24
- package/src/context/keybind.tsx +0 -51
- package/src/context/loading.tsx +0 -68
- package/src/context/sync.tsx +0 -868
- package/src/context/theme.tsx +0 -90
- package/src/context/types.ts +0 -51
- package/src/index.tsx +0 -15
- package/src/keybind/index.ts +0 -2
- package/src/keybind/parser.ts +0 -88
- package/src/keybind/types.ts +0 -83
- package/src/theme/index.ts +0 -3
- package/src/theme/presets/lazygit.ts +0 -45
- package/src/theme/presets/opencode.ts +0 -45
- package/src/theme/types.ts +0 -47
- package/src/utils/double-click.ts +0 -59
- package/src/utils/file-tree.ts +0 -154
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { For, Show } from "solid-js"
|
|
2
|
-
import { useFocus } from "../context/focus"
|
|
3
|
-
import { useTheme } from "../context/theme"
|
|
4
|
-
import { createDoubleClickDetector } from "../utils/double-click"
|
|
5
|
-
import type { FlatFileNode } from "../utils/file-tree"
|
|
6
|
-
|
|
7
|
-
const STATUS_CHARS: Record<string, string> = {
|
|
8
|
-
added: "A",
|
|
9
|
-
modified: "M",
|
|
10
|
-
deleted: "D",
|
|
11
|
-
renamed: "R",
|
|
12
|
-
copied: "C",
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface FileTreeListProps {
|
|
16
|
-
files: () => FlatFileNode[]
|
|
17
|
-
selectedIndex: () => number
|
|
18
|
-
setSelectedIndex: (index: number) => void
|
|
19
|
-
collapsedPaths: () => Set<string>
|
|
20
|
-
toggleFolder: (path: string) => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function FileTreeList(props: FileTreeListProps) {
|
|
24
|
-
const focus = useFocus()
|
|
25
|
-
const { colors } = useTheme()
|
|
26
|
-
|
|
27
|
-
const statusColors = () => ({
|
|
28
|
-
added: colors().success,
|
|
29
|
-
modified: colors().warning,
|
|
30
|
-
deleted: colors().error,
|
|
31
|
-
renamed: colors().info,
|
|
32
|
-
copied: colors().info,
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<For each={props.files()}>
|
|
37
|
-
{(item, index) => {
|
|
38
|
-
const isSelected = () => index() === props.selectedIndex()
|
|
39
|
-
const node = item.node
|
|
40
|
-
const indent = " ".repeat(item.visualDepth)
|
|
41
|
-
const isCollapsed = props.collapsedPaths().has(node.path)
|
|
42
|
-
|
|
43
|
-
const icon = node.isDirectory ? (isCollapsed ? "▶" : "▼") : " "
|
|
44
|
-
|
|
45
|
-
const statusChar = node.status
|
|
46
|
-
? (STATUS_CHARS[node.status] ?? " ")
|
|
47
|
-
: " "
|
|
48
|
-
const statusColor = node.status
|
|
49
|
-
? (statusColors()[
|
|
50
|
-
node.status as keyof ReturnType<typeof statusColors>
|
|
51
|
-
] ?? colors().text)
|
|
52
|
-
: colors().text
|
|
53
|
-
|
|
54
|
-
const handleDoubleClick = createDoubleClickDetector(() => {
|
|
55
|
-
if (node.isDirectory) {
|
|
56
|
-
props.toggleFolder(node.path)
|
|
57
|
-
} else {
|
|
58
|
-
focus.setPanel("detail")
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const handleMouseDown = (e: { stopPropagation: () => void }) => {
|
|
63
|
-
e.stopPropagation()
|
|
64
|
-
props.setSelectedIndex(index())
|
|
65
|
-
if (node.isDirectory) {
|
|
66
|
-
props.toggleFolder(node.path)
|
|
67
|
-
} else {
|
|
68
|
-
handleDoubleClick()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<box
|
|
74
|
-
backgroundColor={
|
|
75
|
-
isSelected() ? colors().selectionBackground : undefined
|
|
76
|
-
}
|
|
77
|
-
overflow="hidden"
|
|
78
|
-
onMouseDown={handleMouseDown}
|
|
79
|
-
>
|
|
80
|
-
<text>
|
|
81
|
-
<span style={{ fg: colors().textMuted }}>{indent}</span>
|
|
82
|
-
<span
|
|
83
|
-
style={{
|
|
84
|
-
fg: node.isDirectory ? colors().info : colors().textMuted,
|
|
85
|
-
}}
|
|
86
|
-
>
|
|
87
|
-
{icon}{" "}
|
|
88
|
-
</span>
|
|
89
|
-
<Show when={!node.isDirectory}>
|
|
90
|
-
<span style={{ fg: statusColor }}>{statusChar} </span>
|
|
91
|
-
</Show>
|
|
92
|
-
<span
|
|
93
|
-
style={{
|
|
94
|
-
fg: node.isDirectory ? colors().info : colors().text,
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
{node.name}
|
|
98
|
-
</span>
|
|
99
|
-
</text>
|
|
100
|
-
</box>
|
|
101
|
-
)
|
|
102
|
-
}}
|
|
103
|
-
</For>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { JSX } from "solid-js"
|
|
2
|
-
import { useTheme } from "../context/theme"
|
|
3
|
-
import { StatusBar } from "./StatusBar"
|
|
4
|
-
import { CommandLogPanel } from "./panels/CommandLogPanel"
|
|
5
|
-
|
|
6
|
-
interface LayoutProps {
|
|
7
|
-
top: JSX.Element
|
|
8
|
-
bottom: JSX.Element
|
|
9
|
-
right: JSX.Element
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function Layout(props: LayoutProps) {
|
|
13
|
-
const { colors, style } = useTheme()
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<box
|
|
17
|
-
flexGrow={1}
|
|
18
|
-
flexDirection="column"
|
|
19
|
-
width="100%"
|
|
20
|
-
height="100%"
|
|
21
|
-
backgroundColor={colors().background}
|
|
22
|
-
padding={style().adaptToTerminal ? 0 : 1}
|
|
23
|
-
gap={0}
|
|
24
|
-
>
|
|
25
|
-
<box flexGrow={1} flexDirection="row" width="100%" gap={0}>
|
|
26
|
-
<box
|
|
27
|
-
flexGrow={1}
|
|
28
|
-
flexBasis={0}
|
|
29
|
-
height="100%"
|
|
30
|
-
flexDirection="column"
|
|
31
|
-
gap={0}
|
|
32
|
-
>
|
|
33
|
-
<box flexGrow={3} flexBasis={0}>
|
|
34
|
-
{props.top}
|
|
35
|
-
</box>
|
|
36
|
-
<box flexGrow={1} flexBasis={0}>
|
|
37
|
-
{props.bottom}
|
|
38
|
-
</box>
|
|
39
|
-
</box>
|
|
40
|
-
<box flexGrow={2} flexBasis={0} height="100%" flexDirection="column">
|
|
41
|
-
<box flexGrow={1}>{props.right}</box>
|
|
42
|
-
<CommandLogPanel />
|
|
43
|
-
</box>
|
|
44
|
-
</box>
|
|
45
|
-
<StatusBar />
|
|
46
|
-
</box>
|
|
47
|
-
)
|
|
48
|
-
}
|
package/src/components/Panel.tsx
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { For, type JSX, Show } from "solid-js"
|
|
2
|
-
import { useCommand } from "../context/command"
|
|
3
|
-
import { useFocus } from "../context/focus"
|
|
4
|
-
import { useTheme } from "../context/theme"
|
|
5
|
-
import type { Context, Panel as PanelType } from "../context/types"
|
|
6
|
-
import { BorderBox } from "./BorderBox"
|
|
7
|
-
|
|
8
|
-
interface Tab {
|
|
9
|
-
id: string
|
|
10
|
-
label: string
|
|
11
|
-
context: Context
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface PanelProps {
|
|
15
|
-
title?: string
|
|
16
|
-
tabs?: Tab[]
|
|
17
|
-
activeTab?: string
|
|
18
|
-
onTabChange?: (tabId: string) => void
|
|
19
|
-
panelId?: PanelType
|
|
20
|
-
hotkey: string
|
|
21
|
-
focused: boolean
|
|
22
|
-
children: JSX.Element
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function Panel(props: PanelProps) {
|
|
26
|
-
const { colors, style } = useTheme()
|
|
27
|
-
const command = useCommand()
|
|
28
|
-
const focus = useFocus()
|
|
29
|
-
|
|
30
|
-
const hasTabs = () => props.tabs && props.tabs.length > 1
|
|
31
|
-
|
|
32
|
-
const cycleTab = (direction: 1 | -1) => {
|
|
33
|
-
if (!props.tabs || props.tabs.length <= 1 || !props.onTabChange) return
|
|
34
|
-
const currentIndex = props.tabs.findIndex((t) => t.id === props.activeTab)
|
|
35
|
-
const nextIndex =
|
|
36
|
-
(currentIndex + direction + props.tabs.length) % props.tabs.length
|
|
37
|
-
const nextTab = props.tabs[nextIndex]
|
|
38
|
-
if (nextTab) props.onTabChange(nextTab.id)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
command.register(() => {
|
|
42
|
-
if (!hasTabs() || !props.panelId) return []
|
|
43
|
-
|
|
44
|
-
return [
|
|
45
|
-
{
|
|
46
|
-
id: `${props.panelId}.next_tab`,
|
|
47
|
-
title: "next tab",
|
|
48
|
-
keybind: "next_tab",
|
|
49
|
-
context: props.panelId,
|
|
50
|
-
type: "navigation",
|
|
51
|
-
panel: props.panelId,
|
|
52
|
-
visibility: "help-only" as const,
|
|
53
|
-
onSelect: () => cycleTab(1),
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: `${props.panelId}.prev_tab`,
|
|
57
|
-
title: "previous tab",
|
|
58
|
-
keybind: "prev_tab",
|
|
59
|
-
context: props.panelId,
|
|
60
|
-
type: "navigation",
|
|
61
|
-
panel: props.panelId,
|
|
62
|
-
visibility: "help-only" as const,
|
|
63
|
-
onSelect: () => cycleTab(-1),
|
|
64
|
-
},
|
|
65
|
-
]
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const renderTitle = () => {
|
|
69
|
-
if (hasTabs()) {
|
|
70
|
-
return (
|
|
71
|
-
<text>
|
|
72
|
-
<Show
|
|
73
|
-
when={props.focused}
|
|
74
|
-
fallback={
|
|
75
|
-
<span style={{ fg: colors().textMuted }}>[{props.hotkey}]</span>
|
|
76
|
-
}
|
|
77
|
-
>
|
|
78
|
-
<span style={{ fg: colors().primary }}>[{props.hotkey}]</span>
|
|
79
|
-
</Show>
|
|
80
|
-
<span style={{ fg: colors().textMuted }}>─</span>
|
|
81
|
-
<For each={props.tabs}>
|
|
82
|
-
{(tab, i) => (
|
|
83
|
-
<>
|
|
84
|
-
<Show when={i() > 0}>
|
|
85
|
-
<span style={{ fg: colors().textMuted }}> </span>
|
|
86
|
-
</Show>
|
|
87
|
-
<span
|
|
88
|
-
style={{
|
|
89
|
-
fg:
|
|
90
|
-
tab.id === props.activeTab
|
|
91
|
-
? colors().primary
|
|
92
|
-
: colors().textMuted,
|
|
93
|
-
}}
|
|
94
|
-
>
|
|
95
|
-
{tab.label}
|
|
96
|
-
</span>
|
|
97
|
-
</>
|
|
98
|
-
)}
|
|
99
|
-
</For>
|
|
100
|
-
</text>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<text>
|
|
106
|
-
<Show
|
|
107
|
-
when={props.focused}
|
|
108
|
-
fallback={
|
|
109
|
-
<span style={{ fg: colors().textMuted }}>
|
|
110
|
-
[{props.hotkey}]─{props.title}
|
|
111
|
-
</span>
|
|
112
|
-
}
|
|
113
|
-
>
|
|
114
|
-
<span style={{ fg: colors().primary }}>
|
|
115
|
-
[{props.hotkey}]─{props.title}
|
|
116
|
-
</span>
|
|
117
|
-
</Show>
|
|
118
|
-
</text>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const handleMouseDown = () => {
|
|
123
|
-
if (props.panelId) {
|
|
124
|
-
focus.setPanel(props.panelId)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<BorderBox
|
|
130
|
-
topLeft={renderTitle}
|
|
131
|
-
border
|
|
132
|
-
borderStyle={style().panel.borderStyle}
|
|
133
|
-
borderColor={props.focused ? colors().borderFocused : colors().border}
|
|
134
|
-
flexGrow={1}
|
|
135
|
-
height="100%"
|
|
136
|
-
overflow="hidden"
|
|
137
|
-
gap={0}
|
|
138
|
-
onMouseDown={handleMouseDown}
|
|
139
|
-
>
|
|
140
|
-
{props.children}
|
|
141
|
-
</BorderBox>
|
|
142
|
-
)
|
|
143
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import type { ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
-
import { useKeyboard } from "@opentui/solid"
|
|
3
|
-
import { For, Show, createEffect, createSignal, onMount } from "solid-js"
|
|
4
|
-
import type { Commit } from "../commander/types"
|
|
5
|
-
import { useTheme } from "../context/theme"
|
|
6
|
-
import { AnsiText } from "./AnsiText"
|
|
7
|
-
|
|
8
|
-
export interface RevisionPickerProps {
|
|
9
|
-
commits: Commit[]
|
|
10
|
-
defaultRevision?: string
|
|
11
|
-
selectedRevision?: string
|
|
12
|
-
onSelect?: (commit: Commit) => void
|
|
13
|
-
focused?: boolean
|
|
14
|
-
height?: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function RevisionPicker(props: RevisionPickerProps) {
|
|
18
|
-
const { colors } = useTheme()
|
|
19
|
-
|
|
20
|
-
const findDefaultIndex = () => {
|
|
21
|
-
if (props.defaultRevision) {
|
|
22
|
-
const idx = props.commits.findIndex(
|
|
23
|
-
(c) => c.changeId === props.defaultRevision,
|
|
24
|
-
)
|
|
25
|
-
return idx >= 0 ? idx : 0
|
|
26
|
-
}
|
|
27
|
-
return 0
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const [selectedIndex, setSelectedIndex] = createSignal(findDefaultIndex())
|
|
31
|
-
|
|
32
|
-
let scrollRef: ScrollBoxRenderable | undefined
|
|
33
|
-
const [scrollTop, setScrollTop] = createSignal(0)
|
|
34
|
-
|
|
35
|
-
const scrollToIndex = (index: number, force = false) => {
|
|
36
|
-
const commitList = props.commits
|
|
37
|
-
if (!scrollRef || commitList.length === 0) return
|
|
38
|
-
|
|
39
|
-
let lineOffset = 0
|
|
40
|
-
const clampedIndex = Math.min(index, commitList.length)
|
|
41
|
-
for (const commit of commitList.slice(0, clampedIndex)) {
|
|
42
|
-
lineOffset += commit.lines.length
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const margin = 2
|
|
46
|
-
const refAny = scrollRef as unknown as Record<string, unknown>
|
|
47
|
-
const viewportHeight =
|
|
48
|
-
(typeof refAny.height === "number" ? refAny.height : null) ??
|
|
49
|
-
(typeof refAny.rows === "number" ? refAny.rows : null) ??
|
|
50
|
-
10
|
|
51
|
-
const currentScrollTop = scrollTop()
|
|
52
|
-
|
|
53
|
-
if (force) {
|
|
54
|
-
const targetScroll = Math.max(0, lineOffset - margin)
|
|
55
|
-
scrollRef.scrollTo(targetScroll)
|
|
56
|
-
setScrollTop(targetScroll)
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const visibleStart = currentScrollTop
|
|
61
|
-
const visibleEnd = currentScrollTop + viewportHeight - 1
|
|
62
|
-
const safeStart = visibleStart + margin
|
|
63
|
-
const safeEnd = visibleEnd - margin
|
|
64
|
-
|
|
65
|
-
let newScrollTop = currentScrollTop
|
|
66
|
-
if (lineOffset < safeStart) {
|
|
67
|
-
newScrollTop = Math.max(0, lineOffset - margin)
|
|
68
|
-
} else if (lineOffset > safeEnd) {
|
|
69
|
-
newScrollTop = Math.max(0, lineOffset - viewportHeight + margin + 1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (newScrollTop !== currentScrollTop) {
|
|
73
|
-
scrollRef.scrollTo(newScrollTop)
|
|
74
|
-
setScrollTop(newScrollTop)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
createEffect(() => {
|
|
79
|
-
const _ = props.commits
|
|
80
|
-
const __ = props.defaultRevision
|
|
81
|
-
setSelectedIndex(findDefaultIndex())
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
onMount(() => {
|
|
85
|
-
setTimeout(() => scrollToIndex(selectedIndex(), true), 1)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
createEffect(() => {
|
|
89
|
-
scrollToIndex(selectedIndex())
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const selectPrev = () => {
|
|
93
|
-
setSelectedIndex((i) => {
|
|
94
|
-
const newIndex = Math.max(0, i - 1)
|
|
95
|
-
const commit = props.commits[newIndex]
|
|
96
|
-
if (commit) props.onSelect?.(commit)
|
|
97
|
-
return newIndex
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const selectNext = () => {
|
|
102
|
-
setSelectedIndex((i) => {
|
|
103
|
-
const newIndex = Math.min(props.commits.length - 1, i + 1)
|
|
104
|
-
const commit = props.commits[newIndex]
|
|
105
|
-
if (commit) props.onSelect?.(commit)
|
|
106
|
-
return newIndex
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
useKeyboard((evt) => {
|
|
111
|
-
if (!props.focused) return
|
|
112
|
-
|
|
113
|
-
if (evt.name === "j" || evt.name === "down") {
|
|
114
|
-
evt.preventDefault()
|
|
115
|
-
selectNext()
|
|
116
|
-
} else if (evt.name === "k" || evt.name === "up") {
|
|
117
|
-
evt.preventDefault()
|
|
118
|
-
selectPrev()
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
createEffect(() => {
|
|
123
|
-
const commit = props.commits[selectedIndex()]
|
|
124
|
-
if (commit) props.onSelect?.(commit)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<Show
|
|
129
|
-
when={props.commits.length > 0}
|
|
130
|
-
fallback={<text fg={colors().textMuted}>No commits</text>}
|
|
131
|
-
>
|
|
132
|
-
<scrollbox
|
|
133
|
-
ref={scrollRef}
|
|
134
|
-
focused={props.focused}
|
|
135
|
-
flexGrow={1}
|
|
136
|
-
height={props.height}
|
|
137
|
-
scrollbarOptions={{ visible: false }}
|
|
138
|
-
>
|
|
139
|
-
<For each={props.commits}>
|
|
140
|
-
{(commit, index) => {
|
|
141
|
-
const isSelected = () => index() === selectedIndex()
|
|
142
|
-
return (
|
|
143
|
-
<For each={commit.lines}>
|
|
144
|
-
{(line) => (
|
|
145
|
-
<box
|
|
146
|
-
backgroundColor={
|
|
147
|
-
isSelected() ? colors().selectionBackground : undefined
|
|
148
|
-
}
|
|
149
|
-
overflow="hidden"
|
|
150
|
-
>
|
|
151
|
-
<AnsiText
|
|
152
|
-
content={line}
|
|
153
|
-
bold={commit.isWorkingCopy}
|
|
154
|
-
wrapMode="none"
|
|
155
|
-
/>
|
|
156
|
-
</box>
|
|
157
|
-
)}
|
|
158
|
-
</For>
|
|
159
|
-
)
|
|
160
|
-
}}
|
|
161
|
-
</For>
|
|
162
|
-
</scrollbox>
|
|
163
|
-
</Show>
|
|
164
|
-
)
|
|
165
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { For, Show, createMemo, createSignal, onCleanup } from "solid-js"
|
|
2
|
-
import { useCommand } from "../context/command"
|
|
3
|
-
import { useDialog } from "../context/dialog"
|
|
4
|
-
import { useFocus } from "../context/focus"
|
|
5
|
-
import { useKeybind } from "../context/keybind"
|
|
6
|
-
import { useLoading } from "../context/loading"
|
|
7
|
-
import { useTheme } from "../context/theme"
|
|
8
|
-
import type { Context } from "../context/types"
|
|
9
|
-
|
|
10
|
-
function contextMatches(
|
|
11
|
-
commandContext: Context,
|
|
12
|
-
activeContext: Context,
|
|
13
|
-
): boolean {
|
|
14
|
-
if (commandContext === "global") return true
|
|
15
|
-
return commandContext === activeContext
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
19
|
-
|
|
20
|
-
export function StatusBar() {
|
|
21
|
-
const command = useCommand()
|
|
22
|
-
const dialog = useDialog()
|
|
23
|
-
const focus = useFocus()
|
|
24
|
-
const keybind = useKeybind()
|
|
25
|
-
const loading = useLoading()
|
|
26
|
-
const { colors, style } = useTheme()
|
|
27
|
-
|
|
28
|
-
const [spinnerFrame, setSpinnerFrame] = createSignal(0)
|
|
29
|
-
|
|
30
|
-
let spinnerInterval: ReturnType<typeof setInterval> | null = null
|
|
31
|
-
const startSpinner = () => {
|
|
32
|
-
if (spinnerInterval) return
|
|
33
|
-
spinnerInterval = setInterval(() => {
|
|
34
|
-
setSpinnerFrame((f) => (f + 1) % SPINNER_FRAMES.length)
|
|
35
|
-
}, 80)
|
|
36
|
-
}
|
|
37
|
-
const stopSpinner = () => {
|
|
38
|
-
if (spinnerInterval) {
|
|
39
|
-
clearInterval(spinnerInterval)
|
|
40
|
-
spinnerInterval = null
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
createMemo(() => {
|
|
45
|
-
if (loading.isLoading()) {
|
|
46
|
-
startSpinner()
|
|
47
|
-
} else {
|
|
48
|
-
stopSpinner()
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
onCleanup(() => stopSpinner())
|
|
53
|
-
|
|
54
|
-
const relevantCommands = createMemo(() => {
|
|
55
|
-
const all = command.all()
|
|
56
|
-
const activeCtx = focus.activeContext()
|
|
57
|
-
const activePanel = focus.panel()
|
|
58
|
-
|
|
59
|
-
const isRelevant = (cmd: (typeof all)[0]) => {
|
|
60
|
-
if (!cmd.keybind) return false
|
|
61
|
-
if (!contextMatches(cmd.context, activeCtx)) return false
|
|
62
|
-
if (cmd.panel && cmd.panel !== activePanel) return false
|
|
63
|
-
return true
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const isVisibleInStatusBar = (cmd: (typeof all)[0]) => {
|
|
67
|
-
const v = cmd.visibility ?? "all"
|
|
68
|
-
return v === "all" || v === "status-only"
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const contextCmds = all.filter(
|
|
72
|
-
(cmd) => isRelevant(cmd) && cmd.context !== "global",
|
|
73
|
-
)
|
|
74
|
-
const globalCmds = all.filter(
|
|
75
|
-
(cmd) => isRelevant(cmd) && cmd.context === "global",
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
const seen = new Set<string>()
|
|
79
|
-
return [...contextCmds, ...globalCmds].filter((cmd) => {
|
|
80
|
-
if (!isVisibleInStatusBar(cmd)) return false
|
|
81
|
-
if (seen.has(cmd.id)) return false
|
|
82
|
-
seen.add(cmd.id)
|
|
83
|
-
return true
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const separator = () => style().statusBar.separator
|
|
88
|
-
const gap = () => (separator() ? 0 : 3)
|
|
89
|
-
|
|
90
|
-
const dialogHints = createMemo(() => {
|
|
91
|
-
const hints = dialog.hints()
|
|
92
|
-
if (hints.length === 0) return []
|
|
93
|
-
return [
|
|
94
|
-
...hints,
|
|
95
|
-
{ key: "esc", label: "close" },
|
|
96
|
-
{ key: "?", label: "help" },
|
|
97
|
-
]
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<box
|
|
102
|
-
height={1}
|
|
103
|
-
flexShrink={0}
|
|
104
|
-
paddingLeft={1}
|
|
105
|
-
paddingRight={1}
|
|
106
|
-
flexDirection="row"
|
|
107
|
-
gap={gap()}
|
|
108
|
-
>
|
|
109
|
-
<Show when={loading.isLoading()}>
|
|
110
|
-
<text>
|
|
111
|
-
<span style={{ fg: colors().warning }}>
|
|
112
|
-
{SPINNER_FRAMES[spinnerFrame()]}
|
|
113
|
-
</span>{" "}
|
|
114
|
-
<span style={{ fg: colors().text }}>{loading.loadingText()}</span>
|
|
115
|
-
<Show when={separator()}>
|
|
116
|
-
<span style={{ fg: colors().textMuted }}>{` ${separator()} `}</span>
|
|
117
|
-
</Show>
|
|
118
|
-
</text>
|
|
119
|
-
</Show>
|
|
120
|
-
<Show
|
|
121
|
-
when={dialog.isOpen() && dialogHints().length > 0}
|
|
122
|
-
fallback={
|
|
123
|
-
<For each={relevantCommands()}>
|
|
124
|
-
{(cmd, index) => (
|
|
125
|
-
<text>
|
|
126
|
-
<span style={{ fg: colors().primary }}>
|
|
127
|
-
{cmd.keybind ? keybind.print(cmd.keybind) : ""}
|
|
128
|
-
</span>{" "}
|
|
129
|
-
<span style={{ fg: colors().text }}>{cmd.title}</span>
|
|
130
|
-
<Show
|
|
131
|
-
when={separator() && index() < relevantCommands().length - 1}
|
|
132
|
-
>
|
|
133
|
-
<span style={{ fg: colors().textMuted }}>
|
|
134
|
-
{` ${separator()} `}
|
|
135
|
-
</span>
|
|
136
|
-
</Show>
|
|
137
|
-
</text>
|
|
138
|
-
)}
|
|
139
|
-
</For>
|
|
140
|
-
}
|
|
141
|
-
>
|
|
142
|
-
<For each={dialogHints()}>
|
|
143
|
-
{(hint, index) => (
|
|
144
|
-
<text>
|
|
145
|
-
<span style={{ fg: colors().primary }}>{hint.key}</span>{" "}
|
|
146
|
-
<span style={{ fg: colors().text }}>{hint.label}</span>
|
|
147
|
-
<Show when={separator() && index() < dialogHints().length - 1}>
|
|
148
|
-
<span style={{ fg: colors().textMuted }}>
|
|
149
|
-
{` ${separator()} `}
|
|
150
|
-
</span>
|
|
151
|
-
</Show>
|
|
152
|
-
</text>
|
|
153
|
-
)}
|
|
154
|
-
</For>
|
|
155
|
-
</Show>
|
|
156
|
-
</box>
|
|
157
|
-
)
|
|
158
|
-
}
|