@kitlangton/ghui 0.1.18 → 0.1.20

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.
@@ -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, runJson, runSchema })
88
+ return CommandRunner.of({ run, runSchema })
81
89
  }),
82
90
  )
83
91
  }