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.
Files changed (50) hide show
  1. package/bin/kajji +60 -0
  2. package/package.json +35 -55
  3. package/script/postinstall.mjs +50 -0
  4. package/LICENSE +0 -21
  5. package/README.md +0 -128
  6. package/bin/kajji.js +0 -2
  7. package/src/App.tsx +0 -229
  8. package/src/commander/bookmarks.ts +0 -129
  9. package/src/commander/diff.ts +0 -186
  10. package/src/commander/executor.ts +0 -285
  11. package/src/commander/files.ts +0 -87
  12. package/src/commander/log.ts +0 -99
  13. package/src/commander/operations.ts +0 -313
  14. package/src/commander/types.ts +0 -21
  15. package/src/components/AnsiText.tsx +0 -77
  16. package/src/components/BorderBox.tsx +0 -124
  17. package/src/components/FileTreeList.tsx +0 -105
  18. package/src/components/Layout.tsx +0 -48
  19. package/src/components/Panel.tsx +0 -143
  20. package/src/components/RevisionPicker.tsx +0 -165
  21. package/src/components/StatusBar.tsx +0 -158
  22. package/src/components/modals/BookmarkNameModal.tsx +0 -170
  23. package/src/components/modals/DescribeModal.tsx +0 -124
  24. package/src/components/modals/HelpModal.tsx +0 -372
  25. package/src/components/modals/RevisionPickerModal.tsx +0 -70
  26. package/src/components/modals/UndoModal.tsx +0 -75
  27. package/src/components/panels/BookmarksPanel.tsx +0 -768
  28. package/src/components/panels/CommandLogPanel.tsx +0 -40
  29. package/src/components/panels/LogPanel.tsx +0 -774
  30. package/src/components/panels/MainArea.tsx +0 -354
  31. package/src/context/command.tsx +0 -106
  32. package/src/context/commandlog.tsx +0 -45
  33. package/src/context/dialog.tsx +0 -217
  34. package/src/context/focus.tsx +0 -63
  35. package/src/context/helper.tsx +0 -24
  36. package/src/context/keybind.tsx +0 -51
  37. package/src/context/loading.tsx +0 -68
  38. package/src/context/sync.tsx +0 -868
  39. package/src/context/theme.tsx +0 -90
  40. package/src/context/types.ts +0 -51
  41. package/src/index.tsx +0 -15
  42. package/src/keybind/index.ts +0 -2
  43. package/src/keybind/parser.ts +0 -88
  44. package/src/keybind/types.ts +0 -83
  45. package/src/theme/index.ts +0 -3
  46. package/src/theme/presets/lazygit.ts +0 -45
  47. package/src/theme/presets/opencode.ts +0 -45
  48. package/src/theme/types.ts +0 -47
  49. package/src/utils/double-click.ts +0 -59
  50. 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
- }
@@ -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
- }