@kitlangton/ghui 0.1.19 → 0.1.21
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/README.md +14 -3
- package/bin/ghui.js +64 -1
- package/package.json +18 -7
- package/src/App.tsx +1075 -653
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +73 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +29 -20
- package/src/errors.ts +10 -0
- package/src/index.tsx +30 -3
- package/src/mergeActions.ts +1 -6
- package/src/pullRequestCache.ts +19 -0
- package/src/pullRequestViews.ts +45 -0
- package/src/services/BrowserOpener.ts +22 -0
- package/src/services/Clipboard.ts +46 -0
- package/src/services/CommandRunner.ts +14 -6
- package/src/services/GitHubService.ts +290 -126
- package/src/services/MockGitHubService.ts +146 -0
- package/src/ui/CommandPalette.tsx +156 -0
- package/src/ui/DetailsPane.tsx +49 -63
- package/src/ui/FooterHints.tsx +84 -179
- package/src/ui/PullRequestDiffPane.tsx +29 -50
- package/src/ui/PullRequestList.tsx +99 -45
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +34 -44
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +263 -295
- package/src/ui/primitives.tsx +90 -0
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect"
|
|
2
|
+
import type { PullRequestItem } from "../domain.js"
|
|
3
|
+
import { CommandRunner, type CommandError } from "./CommandRunner.js"
|
|
4
|
+
|
|
5
|
+
export class BrowserOpener extends Context.Service<BrowserOpener, {
|
|
6
|
+
readonly openPullRequest: (pullRequest: PullRequestItem) => Effect.Effect<void, CommandError>
|
|
7
|
+
}>()("ghui/BrowserOpener") {
|
|
8
|
+
static readonly layerNoDeps = Layer.effect(
|
|
9
|
+
BrowserOpener,
|
|
10
|
+
Effect.gen(function*() {
|
|
11
|
+
const command = yield* CommandRunner
|
|
12
|
+
|
|
13
|
+
const openPullRequest = Effect.fn("BrowserOpener.openPullRequest")(function*(pullRequest: PullRequestItem) {
|
|
14
|
+
yield* command.run("gh", ["pr", "view", String(pullRequest.number), "--repo", pullRequest.repository, "--web"])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return BrowserOpener.of({ openPullRequest })
|
|
18
|
+
}),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
static readonly layer = BrowserOpener.layerNoDeps.pipe(Layer.provide(CommandRunner.layer))
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Schema } from "effect"
|
|
2
|
+
import { CommandRunner } from "./CommandRunner.js"
|
|
3
|
+
|
|
4
|
+
export class ClipboardError extends Schema.TaggedErrorClass<ClipboardError>()("ClipboardError", {
|
|
5
|
+
detail: Schema.String,
|
|
6
|
+
}) {}
|
|
7
|
+
|
|
8
|
+
const clipboardCommands: readonly (readonly [string, ...readonly string[]])[] =
|
|
9
|
+
process.platform === "darwin" ? [["pbcopy"]]
|
|
10
|
+
: process.platform === "linux" ? [
|
|
11
|
+
...(process.env.WAYLAND_DISPLAY ? [["wl-copy"] as const] : []),
|
|
12
|
+
["xclip", "-selection", "clipboard"] as const,
|
|
13
|
+
["xsel", "--clipboard", "--input"] as const,
|
|
14
|
+
]
|
|
15
|
+
: []
|
|
16
|
+
|
|
17
|
+
const installHint = process.platform === "linux" ? " Install wl-clipboard, xclip, or xsel." : ""
|
|
18
|
+
const unavailableDetail = `Clipboard is not available.${installHint}`
|
|
19
|
+
|
|
20
|
+
export class Clipboard extends Context.Service<Clipboard, {
|
|
21
|
+
readonly copy: (text: string) => Effect.Effect<void, ClipboardError>
|
|
22
|
+
}>()("ghui/Clipboard") {
|
|
23
|
+
static readonly layerNoDeps = Layer.effect(
|
|
24
|
+
Clipboard,
|
|
25
|
+
Effect.gen(function*() {
|
|
26
|
+
const command = yield* CommandRunner
|
|
27
|
+
|
|
28
|
+
const copy = Effect.fn("Clipboard.copy")(function*(text: string) {
|
|
29
|
+
if (clipboardCommands.length === 0) {
|
|
30
|
+
return yield* new ClipboardError({ detail: unavailableDetail })
|
|
31
|
+
}
|
|
32
|
+
let lastDetail = ""
|
|
33
|
+
for (const [cmd, ...args] of clipboardCommands) {
|
|
34
|
+
const result = yield* command.run(cmd, args, { stdin: text }).pipe(Effect.result)
|
|
35
|
+
if (result._tag === "Success") return
|
|
36
|
+
lastDetail = result.failure.detail
|
|
37
|
+
}
|
|
38
|
+
return yield* new ClipboardError({ detail: lastDetail || unavailableDetail })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return Clipboard.of({ copy })
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
static readonly layer = Clipboard.layerNoDeps.pipe(Layer.provide(CommandRunner.layer))
|
|
46
|
+
}
|
|
@@ -6,6 +6,10 @@ export interface CommandResult {
|
|
|
6
6
|
readonly exitCode: number
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface RunOptions {
|
|
10
|
+
readonly stdin?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export class CommandError extends Schema.TaggedErrorClass<CommandError>()("CommandError", {
|
|
10
14
|
command: Schema.String,
|
|
11
15
|
args: Schema.Array(Schema.String),
|
|
@@ -26,22 +30,26 @@ const readStream = async (stream: ReadableStream | null | undefined) => {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export class CommandRunner extends Context.Service<CommandRunner, {
|
|
29
|
-
readonly run: (command: string, args: readonly string[]) => Effect.Effect<CommandResult, CommandError>
|
|
30
|
-
readonly runJson: <A>(command: string, args: readonly string[]) => Effect.Effect<A, CommandError | JsonParseError>
|
|
33
|
+
readonly run: (command: string, args: readonly string[], options?: RunOptions) => Effect.Effect<CommandResult, CommandError>
|
|
31
34
|
readonly runSchema: <S extends Schema.Top>(schema: S, command: string, args: readonly string[]) =>
|
|
32
35
|
Effect.Effect<S["Type"], CommandError | JsonParseError | Schema.SchemaError, S["DecodingServices"]>
|
|
33
36
|
}>()("ghui/CommandRunner") {
|
|
34
37
|
static readonly layer = Layer.effect(
|
|
35
38
|
CommandRunner,
|
|
36
39
|
Effect.gen(function*() {
|
|
37
|
-
const runProcess = Effect.fn("CommandRunner.runProcess")((command: string, args: readonly string[]) =>
|
|
40
|
+
const runProcess = Effect.fn("CommandRunner.runProcess")((command: string, args: readonly string[], stdin: string | undefined) =>
|
|
38
41
|
Effect.tryPromise({
|
|
39
42
|
async try() {
|
|
40
43
|
const proc = Bun.spawn({
|
|
41
44
|
cmd: [command, ...args],
|
|
45
|
+
stdin: stdin === undefined ? "ignore" : "pipe",
|
|
42
46
|
stdout: "pipe",
|
|
43
47
|
stderr: "pipe",
|
|
44
48
|
})
|
|
49
|
+
if (stdin !== undefined && proc.stdin) {
|
|
50
|
+
proc.stdin.write(stdin)
|
|
51
|
+
proc.stdin.end()
|
|
52
|
+
}
|
|
45
53
|
|
|
46
54
|
const [exitCode, stdout, stderr] = await Promise.all([proc.exited, readStream(proc.stdout), readStream(proc.stderr)])
|
|
47
55
|
return { stdout, stderr, exitCode }
|
|
@@ -50,8 +58,8 @@ export class CommandRunner extends Context.Service<CommandRunner, {
|
|
|
50
58
|
})
|
|
51
59
|
)
|
|
52
60
|
|
|
53
|
-
const run = Effect.fn("CommandRunner.run")(function*(command: string, args: readonly string[]) {
|
|
54
|
-
const result = yield* runProcess(command, args).pipe(Effect.withSpan("ghui.command.runProcess", {
|
|
61
|
+
const run = Effect.fn("CommandRunner.run")(function*(command: string, args: readonly string[], options?: RunOptions) {
|
|
62
|
+
const result = yield* runProcess(command, args, options?.stdin).pipe(Effect.withSpan("ghui.command.runProcess", {
|
|
55
63
|
attributes: {
|
|
56
64
|
"process.command": command,
|
|
57
65
|
"process.argv.count": args.length,
|
|
@@ -77,7 +85,7 @@ export class CommandRunner extends Context.Service<CommandRunner, {
|
|
|
77
85
|
return yield* Schema.decodeUnknownEffect(schema)(value)
|
|
78
86
|
})
|
|
79
87
|
|
|
80
|
-
return CommandRunner.of({ run,
|
|
88
|
+
return CommandRunner.of({ run, runSchema })
|
|
81
89
|
}),
|
|
82
90
|
)
|
|
83
91
|
}
|