Open on another machine from the window; keep the graph when a reload fails
This commit is contained in:
+3
-1
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user