Merge branch 'dev' of https://github.com/sst/opencode into dev
Some checks are pending
deploy / deploy (push) Waiting to run
Update Nix Hashes / update (push) Waiting to run

This commit is contained in:
David Hill 2025-12-11 19:34:34 +00:00
commit afcd547a16
10 changed files with 174 additions and 57 deletions

View file

@ -85,7 +85,6 @@ jobs:
publish-tauri:
needs: publish
if: inputs.bump || inputs.version
continue-on-error: true
strategy:
fail-fast: false
@ -130,7 +129,7 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: install dependencies (ubuntu only)
if: startsWith(matrix.settings.host, 'ubuntu')
if: contains(matrix.settings.host, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
@ -162,7 +161,7 @@ jobs:
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
if: startsWith(matrix.settings.host, 'ubuntu')
if: contains(matrix.settings.host, 'ubuntu')
- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
@ -180,7 +179,7 @@ jobs:
with:
projectPath: packages/tauri
uploadWorkflowArtifacts: true
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }}
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.releaseId }}

View file

@ -199,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) {
<DialogSelect
keybind={[
{
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
keybind: Keybind.parse("ctrl+a")[0],
title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() => <DialogProvider />)

View file

@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync"
import { Identifier } from "@/id/id"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
import { Keybind } from "@/util/keybind"
import { usePromptHistory, type PromptInfo } from "./history"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
@ -85,7 +86,7 @@ const TEXTAREA_ACTIONS = [
] as const
function mapTextareaKeybindings(
keybinds: Record<string, { ctrl: boolean; meta: boolean; shift: boolean; leader: boolean; name: string }[]>,
keybinds: Record<string, Keybind.Info[]>,
action: (typeof TEXTAREA_ACTIONS)[number],
): KeyBinding[] {
const configKey = `input_${action.replace(/-/g, "_")}`
@ -96,6 +97,7 @@ function mapTextareaKeybindings(
ctrl: binding.ctrl || undefined,
meta: binding.meta || undefined,
shift: binding.shift || undefined,
super: binding.super || undefined,
action,
}))
}

View file

@ -73,21 +73,11 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
return store.leader
},
parse(evt: ParsedKey): Keybind.Info {
if (evt.name === "\x1F")
return {
ctrl: true,
name: "_",
shift: false,
leader: false,
meta: false,
}
return {
ctrl: evt.ctrl,
name: evt.name,
shift: evt.shift,
leader: store.leader,
meta: evt.meta,
// Handle special case for Ctrl+Underscore (represented as \x1F)
if (evt.name === "\x1F") {
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
}
return Keybind.fromParsedKey(evt, store.leader)
},
match(key: keyof KeybindsConfig, evt: ParsedKey) {
const keybind = keybinds()[key]

View file

@ -512,8 +512,8 @@ export namespace Config {
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
input_undo: z.string().optional().default("ctrl+-").describe("Undo in input"),
input_redo: z.string().optional().default("ctrl+.").describe("Redo in input"),
input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
input_word_forward: z
.string()
.optional()

View file

@ -1,16 +1,35 @@
import { isDeepEqual } from "remeda"
import type { ParsedKey } from "@opentui/core"
export namespace Keybind {
export type Info = {
ctrl: boolean
meta: boolean
shift: boolean
leader: boolean
name: string
/**
* Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
* This ensures type compatibility and catches missing fields at compile time.
*/
export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
leader: boolean // our custom field
}
export function match(a: Info, b: Info): boolean {
return isDeepEqual(a, b)
// Normalize super field (undefined and false are equivalent)
const normalizedA = { ...a, super: a.super ?? false }
const normalizedB = { ...b, super: b.super ?? false }
return isDeepEqual(normalizedA, normalizedB)
}
/**
* Convert OpenTUI's ParsedKey to our Keybind.Info format.
* This helper ensures all required fields are present and avoids manual object creation.
*/
export function fromParsedKey(key: ParsedKey, leader = false): Info {
return {
name: key.name,
ctrl: key.ctrl,
meta: key.meta,
shift: key.shift,
super: key.super ?? false,
leader,
}
}
export function toString(info: Info): string {
@ -18,6 +37,7 @@ export namespace Keybind {
if (info.ctrl) parts.push("ctrl")
if (info.meta) parts.push("alt")
if (info.super) parts.push("super")
if (info.shift) parts.push("shift")
if (info.name) {
if (info.name === "delete") parts.push("del")
@ -58,6 +78,9 @@ export namespace Keybind {
case "option":
info.meta = true
break
case "super":
info.super = true
break
case "shift":
info.shift = true
break

View file

@ -68,6 +68,31 @@ describe("Keybind.toString", () => {
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: true, name: "" }
expect(Keybind.toString(info)).toBe("<leader>")
})
test("should convert super modifier to string", () => {
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
expect(Keybind.toString(info)).toBe("super+z")
})
test("should convert super+shift modifier to string", () => {
const info: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
expect(Keybind.toString(info)).toBe("super+shift+z")
})
test("should handle super with ctrl modifier", () => {
const info: Keybind.Info = { ctrl: true, meta: false, shift: false, super: true, leader: false, name: "a" }
expect(Keybind.toString(info)).toBe("ctrl+super+a")
})
test("should handle super with all modifiers", () => {
const info: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "x" }
expect(Keybind.toString(info)).toBe("ctrl+alt+super+shift+x")
})
test("should handle undefined super field (omitted)", () => {
const info: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
expect(Keybind.toString(info)).toBe("ctrl+c")
})
})
describe("Keybind.match", () => {
@ -118,6 +143,36 @@ describe("Keybind.match", () => {
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: false, name: "a" }
expect(Keybind.match(a, b)).toBe(true)
})
test("should match super modifier keybinds", () => {
const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
expect(Keybind.match(a, b)).toBe(true)
})
test("should not match super vs non-super", () => {
const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: false, leader: false, name: "z" }
expect(Keybind.match(a, b)).toBe(false)
})
test("should match undefined super with false super", () => {
const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "c" }
expect(Keybind.match(a, b)).toBe(true)
})
test("should match super+shift combination", () => {
const a: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
const b: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
expect(Keybind.match(a, b)).toBe(true)
})
test("should not match when only super differs", () => {
const a: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "a" }
const b: Keybind.Info = { ctrl: true, meta: true, shift: true, super: false, leader: false, name: "a" }
expect(Keybind.match(a, b)).toBe(false)
})
})
describe("Keybind.parse", () => {
@ -314,4 +369,53 @@ describe("Keybind.parse", () => {
},
])
})
test("should parse super modifier", () => {
const result = Keybind.parse("super+z")
expect(result).toEqual([
{
ctrl: false,
meta: false,
shift: false,
super: true,
leader: false,
name: "z",
},
])
})
test("should parse super with shift modifier", () => {
const result = Keybind.parse("super+shift+z")
expect(result).toEqual([
{
ctrl: false,
meta: false,
shift: true,
super: true,
leader: false,
name: "z",
},
])
})
test("should parse multiple keybinds with super", () => {
const result = Keybind.parse("ctrl+-,super+z")
expect(result).toEqual([
{
ctrl: true,
meta: false,
shift: false,
leader: false,
name: "-",
},
{
ctrl: false,
meta: false,
shift: false,
super: true,
leader: false,
name: "z",
},
])
})
})

View file

@ -7333,12 +7333,12 @@
},
"input_undo": {
"description": "Undo in input",
"default": "ctrl+-",
"default": "ctrl+-,super+z",
"type": "string"
},
"input_redo": {
"description": "Redo in input",
"default": "ctrl+.",
"default": "ctrl+.,super+shift+z",
"type": "string"
},
"input_word_forward": {

View file

@ -307,19 +307,18 @@
--border-weaker-focus: var(--smoke-light-alpha-6);
--button-ghost-hover: var(--smoke-light-alpha-2);
--button-ghost-hover2: var(--smoke-light-alpha-3);
--avatar-background-pink: #FEEEF8;
--avatar-background-mint: #E1FBF4 ;
--avatar-background-orange: #FFF1E7 ;
--avatar-background-purple: #F9F1FE;
--avatar-background-cyan: #E7F9FB;
--avatar-background-lime: #EEFADC;
--avatar-text-pink: #CD1D8D;
--avatar-text-mint: #147D6F ;
--avatar-text-orange: #ED5F00 ;
--avatar-text-purple: #8445BC;
--avatar-text-cyan: #0894B3;
--avatar-text-lime: #5D770D;
--avatar-background-pink: #feeef8;
--avatar-background-mint: #e1fbf4;
--avatar-background-orange: #fff1e7;
--avatar-background-purple: #f9f1fe;
--avatar-background-cyan: #e7f9fb;
--avatar-background-lime: #eefadc;
--avatar-text-pink: #cd1d8d;
--avatar-text-mint: #147d6f;
--avatar-text-orange: #ed5f00;
--avatar-text-purple: #8445bc;
--avatar-text-cyan: #0894b3;
--avatar-text-lime: #5d770d;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
@ -564,18 +563,18 @@
--border-weaker-focus: var(--smoke-dark-alpha-6);
--button-ghost-hover: var(--smoke-dark-alpha-2);
--button-ghost-hover2: var(--smoke-dark-alpha-3);
--avatar-background-pink: #501B3F;
--avatar-background-mint: #033A34;
--avatar-background-orange: #5F2A06;
--avatar-background-pink: #501b3f;
--avatar-background-mint: #033a34;
--avatar-background-orange: #5f2a06;
--avatar-background-purple: #432155;
--avatar-background-cyan: #0F3058;
--avatar-background-lime: #2B3711;
--avatar-text-pink: #E34BA9;
--avatar-text-mint: #95F3D9 ;
--avatar-text-orange: #FF802B ;
--avatar-text-purple: #9D5BD2;
--avatar-text-cyan: #369EFF;
--avatar-text-lime: #C4F042;
--avatar-background-cyan: #0f3058;
--avatar-background-lime: #2b3711;
--avatar-text-pink: #e34ba9;
--avatar-text-mint: #95f3d9;
--avatar-text-orange: #ff802b;
--avatar-text-purple: #9d5bd2;
--avatar-text-cyan: #369eff;
--avatar-text-lime: #c4f042;
}
}

View file

@ -73,8 +73,8 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
"input_delete_to_line_start": "ctrl+u",
"input_backspace": "backspace,shift+backspace",
"input_delete": "ctrl+d,delete,shift+delete",
"input_undo": "ctrl+-,cmd+z",
"input_redo": "ctrl+.,cmd+shift+z",
"input_undo": "ctrl+-,super+z",
"input_redo": "ctrl+.,super+shift+z",
"input_word_forward": "alt+f,alt+right,ctrl+right",
"input_word_backward": "alt+b,alt+left,ctrl+left",
"input_select_word_forward": "alt+shift+f,alt+shift+right",