Let error messages stay until read; show the remote form on the welcome screen

Error messages now stay for 8 seconds or until clicked, and keep each
hint on its own line. The welcome screen shows how to start gites with
a repository on another machine. The spec follows these changes and the
new ssh options and socket folder.
This commit is contained in:
2026-10-15 15:26:16 +02:00
parent bf6ae3e2bb
commit 7b902616fe
3 changed files with 39 additions and 11 deletions
@@ -35,7 +35,7 @@ one character, and no local folder with exactly that name exists. Every other te
local folder. So `C:\code\x` and `C:/code/x` stay local, `./a:b` stays local, and a
folder in the current folder named `notes:2024` stays local when it exists.
A path on the server that starts with `~/` is handed to the server's shell with the
A path on the server that is `~` or starts with `~/` is handed to the server's shell with the
`~` outside the quotes, so the shell expands it to the home folder there.
## Running git
@@ -49,6 +49,7 @@ A path on the server that starts with `~/` is handed to the server's shell with
```
ssh -o BatchMode=yes -o ConnectTimeout=10
-o ServerAliveInterval=5 -o ServerAliveCountMax=2
-o ControlMaster=auto -o ControlPath=<socket> -o ControlPersist=10m
user@myserver "git -C '/srv/app' 'log' '--format=…' …"
```
@@ -57,12 +58,18 @@ A path on the server that starts with `~/` is handed to the server's shell with
has no terminal to ask in. Keys and the ssh agent work.
- `ConnectTimeout=10`: a server that does not answer gives an error after 10 seconds
instead of a window that waits for ever.
- `ServerAliveInterval=5`, `ServerAliveCountMax=2`: when a server stops answering
on a connection that is already open (also a shared one), ssh notices it in about
10 seconds and gives an error instead of waiting for ever.
- `ControlMaster`, `ControlPath`, `ControlPersist`: the first command opens one
connection and leaves it open; later commands go through it and cost about one
network round trip each instead of a new login. The connection closes 10 minutes
after the last command. The socket is `gites-%C` in the user's runtime folder
(`/run/user/<id>` on Linux), or in `/tmp` where there is none; `%C` is a hash ssh
makes from the host, port and user, so the path stays short.
(`/run/user/<id>` on Linux). Where there is none (always on macOS), it is in a
`gites` folder in the user's cache folder, made so that only the user can open it;
never in `/tmp`. If neither folder can be found or made, these three options are
left out and every command opens a new connection. `%C` is a hash ssh makes from
the host, port and user, so the path stays short.
- On Windows, ssh cannot share connections, so these three options are left out and
every command opens a new connection.
- Quoting: every word of the git command is put in single quotes, and a `'` inside a
@@ -107,6 +114,12 @@ If the connection drops while a repository is open, the next command shows its e
as a message at the bottom of the window and the graph that is already on screen
stays. The next reload connects again by itself.
A message that reports an error stays for 8 seconds, or until it is clicked, and each
hint line above stays on its own line. Other messages go after 1.8 seconds, as before.
The commands that run git run away from the thread that draws the window, so the
window does not freeze while it waits for a slow server.
## Tests
- Reading locations: `host:/path`, `user@host:/path`, `host:~/x`, `C:\x`, `C:/x`, a
+19 -7
View File
@@ -70,7 +70,7 @@ watch(
if (!settingsLoaded) return;
clearTimeout(settingsTimer);
settingsTimer = window.setTimeout(() => {
invoke("save_settings", { value: { ...settings } }).catch((e) => showToast(`could not save settings: ${e}`));
invoke("save_settings", { value: { ...settings } }).catch((e) => showToast(`could not save settings: ${e}`, { error: true }));
}, 300);
},
{ deep: true },
@@ -117,7 +117,7 @@ async function load() {
if (data) await applyRepo(data);
} catch (e) {
// A repository already on screen stays, for example when a connection drops.
if (repo.value) showToast(`could not reload: ${e}`);
if (repo.value) showToast(`could not reload: ${e}`, { error: true });
else error.value = String(e);
} finally {
loading = false;
@@ -131,7 +131,7 @@ async function openRepo(path: string) {
await applyRepo(await invoke<RepoData>("open_repo", { path }));
} catch (e) {
// The repository that was open stays open.
showToast(`could not open ${path}: ${e}`);
showToast(`could not open ${path}: ${e}`, { error: true });
}
}
@@ -177,7 +177,7 @@ watch(
invoke("save_filters", {
repo: repoRoot,
value: { hidden: filters.hidden, rules: filters.rules, hideTags: filters.hideTags, collapseAll: collapseAll.value },
}).catch((e) => showToast(`could not save filters: ${e}`));
}).catch((e) => showToast(`could not save filters: ${e}`, { error: true }));
}, 300);
},
{ deep: true },
@@ -366,10 +366,17 @@ async function copyText(text: string) {
// ---- actions -------------------------------------------------------------
let toastTimer: number | undefined;
function showToast(text: string) {
/** Shows a short message. An error message stays longer, 8 seconds, so there is time to
* read it; a click on any message closes it. */
function showToast(text: string, { error = false } = {}) {
toast.value = text;
clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => (toast.value = null), 1800);
toastTimer = window.setTimeout(() => (toast.value = null), error ? 8000 : 1800);
}
function closeToast() {
clearTimeout(toastTimer);
toast.value = null;
}
async function copyHash() {
@@ -653,7 +660,7 @@ onUnmounted(() => {
</footer>
<transition name="fade">
<div v-if="toast" class="toast">{{ toast }}</div>
<div v-if="toast" class="toast" @click="closeToast">{{ toast }}</div>
</transition>
</div>
</template>
@@ -760,6 +767,11 @@ onUnmounted(() => {
padding: 6px 14px;
border-radius: 8px;
box-shadow: 0 6px 24px #0008;
/* Keeps each line of a message on its own line, such as the hint under an ssh error. */
white-space: pre-line;
max-width: min(640px, calc(100vw - 32px));
overflow-wrap: anywhere;
cursor: pointer;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
+4 -1
View File
@@ -56,7 +56,10 @@ const name = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p;
/>
<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>
<p class="hint">
You can also start gites with a path: <code>gites ~/code/my-project</code>,
or with a repository on another machine: <code>gites myserver:/srv/app</code>
</p>
</div>
</div>
</template>