Open on another machine from the window; keep the graph when a reload fails

This commit is contained in:
2026-10-15 15:15:41 +02:00
parent 16b0363d07
commit ccf945a067
3 changed files with 87 additions and 7 deletions
+3 -1
View File
@@ -116,7 +116,9 @@ async function load() {
const data = await invoke<RepoData | null>("load_repo");
if (data) await applyRepo(data);
} catch (e) {
error.value = String(e);
// A repository already on screen stays, for example when a connection drops.
if (repo.value) showToast(`could not reload: ${e}`);
else error.value = String(e);
} finally {
loading = false;
started.value = true;
+49 -5
View File
@@ -1,29 +1,54 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { computed, nextTick, onMounted, ref } from "vue";
const props = defineProps<{ recents: string[]; current: string | null }>();
const emit = defineEmits<{ pick: [path: string]; browse: []; remove: [path: string]; close: [] }>();
// The current repository is not offered: picking it would do nothing.
const items = computed(() => props.recents.filter((p) => p !== props.current));
/** Index into items; items.length means the "Open folder…" entry. */
/** Index into items; items.length is the "Open folder…" entry and items.length + 1 the "Open on another machine…" entry. */
const active = ref(0);
const menu = ref<HTMLElement>();
onMounted(() => menu.value?.focus());
/** True while the "Open on another machine…" entry is a text field. */
const typing = ref(false);
const remoteText = ref("");
const remoteInput = ref<HTMLInputElement>();
async function startTyping() {
typing.value = true;
await nextTick();
remoteInput.value?.focus();
}
function choose(i: number) {
if (i >= items.value.length) emit("browse");
if (i === items.value.length + 1) void startTyping();
else if (i === items.value.length) emit("browse");
else emit("pick", items.value[i]);
}
function onRemoteKey(e: KeyboardEvent) {
// Typed letters belong to the field, not to the menu's own keys.
e.stopPropagation();
if (e.key === "Enter") {
const text = remoteText.value.trim();
if (text) emit("pick", text);
} else if (e.key === "Escape") {
typing.value = false;
menu.value?.focus();
} else return;
e.preventDefault();
}
function onKey(e: KeyboardEvent) {
// Keys stay in the menu while it is open.
e.stopPropagation();
const last = items.value.length;
const last = items.value.length + 1;
if (e.key === "ArrowDown" || e.key === "j") active.value = Math.min(last, active.value + 1);
else if (e.key === "ArrowUp" || e.key === "k") active.value = Math.max(0, active.value - 1);
else if (e.key === "Enter") choose(active.value);
else if (e.key === "Delete" && active.value < last) emit("remove", items.value[active.value]);
else if (e.key === "Delete" && active.value < items.value.length) emit("remove", items.value[active.value]);
else if (e.key === "Escape" || e.key === "o") emit("close");
else return;
e.preventDefault();
@@ -57,6 +82,23 @@ const name = (p: string) => p.split("/").filter(Boolean).pop() ?? p;
>
Open folder…
</div>
<div
class="item remote"
:class="{ active: active === items.length + 1 }"
@mouseenter="active = items.length + 1"
@click="!typing && choose(items.length + 1)"
>
<input
v-if="typing"
ref="remoteInput"
v-model="remoteText"
class="remote-input"
placeholder="user@host:/path/to/repo"
spellcheck="false"
@keydown="onRemoteKey"
/>
<template v-else>Open on another machine…</template>
</div>
<div class="foot"><kbd>↑</kbd><kbd>↓</kbd> choose · <kbd>Enter</kbd> open · <kbd>Del</kbd> remove · <kbd>Esc</kbd> close</div>
</div>
</template>
@@ -87,5 +129,7 @@ const name = (p: string) => p.split("/").filter(Boolean).pop() ?? p;
.x { border: none; padding: 0 6px; color: var(--text-dim); opacity: 0; }
.item.active .x { opacity: 1; }
.browse { color: var(--accent); border-top: 1px solid var(--border); border-radius: 0 0 6px 6px; margin-top: 4px; }
.remote { color: var(--accent); }
.remote-input { flex: 1; min-width: 0; font: 12px ui-monospace, monospace; }
.foot { padding: 6px 10px 2px; font-size: 11px; color: var(--text-dim); }
</style>
+35 -1
View File
@@ -1,7 +1,27 @@
<script setup lang="ts">
import { nextTick, ref } from "vue";
defineProps<{ recents: string[] }>();
const emit = defineEmits<{ pick: [path: string]; browse: []; remove: [path: string] }>();
/** True while the "Open on another machine…" button is a text field. */
const typing = ref(false);
const remoteText = ref("");
const remoteInput = ref<HTMLInputElement>();
async function startTyping() {
typing.value = true;
await nextTick();
remoteInput.value?.focus();
}
function onRemoteKey(e: KeyboardEvent) {
if (e.key === "Enter") {
const text = remoteText.value.trim();
if (text) emit("pick", text);
} else if (e.key === "Escape") typing.value = false;
}
// Paths on Windows use backslashes.
const name = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p;
</script>
@@ -23,7 +43,19 @@ const name = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p;
</div>
</div>
</template>
<button class="browse" @click="emit('browse')">Choose a folder…</button>
<div class="actions">
<button class="browse" @click="emit('browse')">Choose a folder…</button>
<input
v-if="typing"
ref="remoteInput"
v-model="remoteText"
class="remote-input"
placeholder="user@host:/path/to/repo"
spellcheck="false"
@keydown="onRemoteKey"
/>
<button v-else class="browse" @click="startTyping">Open on another machine…</button>
</div>
<p class="hint">You can also start gites with a path: <code>gites ~/code/my-project</code></p>
</div>
</div>
@@ -44,6 +76,8 @@ h2 { margin: 0 0 4px; font-size: 20px; }
.path { flex: 1; min-width: 0; color: var(--text-dim); font: 11.5px ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.x { border: none; padding: 2px 10px; color: var(--text-dim); opacity: 0; }
.item:hover .x, .item:focus-within .x { opacity: 1; }
.actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.remote-input { flex: 1; min-width: 220px; font: 12px ui-monospace, monospace; }
.browse { align-self: flex-start; padding: 6px 14px; color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
.hint { margin: 4px 0 0; color: var(--text-dim); font-size: 12px; }
.hint code { font-family: ui-monospace, monospace; color: var(--text); }