Simplify file view layout and fix double-escaping
- Reuse tree view's two-column layout (section nav + position token in
shared nav row, sidebar on right, code in main column)
- Move blob metadata (lines, size, language, mode, encoding) into sidebar
side-blocks instead of a separate strip above the code
- Move last change info into a sidebar side-block
- Move raw/blame/history actions into a sidebar side-block
- Add file path breadcrumbs to the top nav (tree > dir > file)
- Fix HTML entity double-escaping: pre-escape lines in handler, use |safe
filter in template to prevent Askama from re-escaping
Assisted-by: Owl Alpha via pi
diff --git a/quire-server/src/quire/web/handlers/file.rs b/quire-server/src/quire/web/handlers/file.rs
index 192105e..f0df3aa 100644
--- a/quire-server/src/quire/web/handlers/file.rs
+++ b/quire-server/src/quire/web/handlers/file.rs
@@ -5,7 +5,7 @@ use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use super::super::db;
-use super::super::templates::{FileViewTemplate, nav_sections};
+use super::super::templates::{Crumb, FileViewTemplate, nav_sections};
use super::git::RepoView;
use super::render;
use crate::Quire;
@@ -34,10 +34,12 @@ pub async fn file_view(
None => return StatusCode::NOT_FOUND.into_response(),
};
+ let crumbs = build_path_crumbs(&repo_display, &path);
+
let tmpl = FileViewTemplate {
sections: nav_sections(&repo_display, "tree", false),
repo: repo_display,
- crumbs: vec![],
+ crumbs,
path,
bookmark: file_data.bookmark,
sha_short: file_data.sha_short.clone(),
@@ -206,3 +208,22 @@ fn html_escape(s: &str) -> String {
.replace('>', ">")
.replace('"', """)
}
+
+fn build_path_crumbs(repo: &str, path: &str) -> Vec<Crumb> {
+ let mut crumbs = vec![Crumb::with_href("tree", format!("/{}/tree", repo))];
+ if path.is_empty() {
+ return crumbs;
+ }
+ let segments: Vec<&str> = path.split('/').collect();
+ for (i, seg) in segments.iter().enumerate() {
+ let href = if i == segments.len() - 1 {
+ // Last segment — link to the blob view.
+ format!("/{}/blob/{}", repo, segments[..=i].join("/"))
+ } else {
+ // Intermediate segment — link to the tree view.
+ format!("/{}/tree/{}", repo, segments[..=i].join("/"))
+ };
+ crumbs.push(Crumb::with_href(*seg, href));
+ }
+ crumbs
+}
diff --git a/quire-server/static/style.css b/quire-server/static/style.css
index 5a767fc..9b20cd5 100644
--- a/quire-server/static/style.css
+++ b/quire-server/static/style.css
@@ -699,77 +699,62 @@ html.dark {
/* ── File view ─────────────────────────────────────────────────── */
-.file-meta {
- padding: 10px 56px;
- border-bottom: 1px solid var(--rule);
+.file-code-col {
+ min-width: 0;
+ border-right: 1px solid var(--rule);
+}
+
+/* File info sidebar blocks */
+
+.file-info-list {
font-family: var(--font-mono);
font-size: 12.5px;
- display: flex;
- align-items: baseline;
- gap: 8px;
- flex-wrap: wrap;
- color: var(--muted);
+ line-height: 1.8;
}
-.file-meta-item {
- display: inline-flex;
- align-items: center;
- gap: 6px;
+.file-info-row {
+ display: flex;
+ align-items: baseline;
+ gap: 12px;
}
-.file-meta-key {
+.file-info-key {
+ color: var(--mutedFaint);
font-size: 10px;
letter-spacing: 0.8px;
text-transform: uppercase;
- color: var(--mutedFaint);
-}
-
-.file-meta-val {
- color: var(--muted);
-}
-
-.file-meta-link {
- color: var(--accent);
- text-decoration: none;
- border-bottom: 1px dotted var(--rule2);
-}
-
-.file-meta-spacer {
- flex: 1;
-}
-
-.file-actions {
- display: inline-flex;
- gap: 14px;
+ min-width: 70px;
}
-.file-action-link {
- color: var(--accent);
- text-decoration: none;
-}
-
-.file-action-link--muted {
+.file-info-val {
color: var(--muted);
}
-/* Last change strip */
+/* Last change sidebar block */
-.file-last-change {
- padding: 10px 56px;
- border-bottom: 1px solid var(--rule2);
+.file-last-change-side {
font-family: var(--font-mono);
font-size: 12.5px;
- display: flex;
- gap: 16px;
- align-items: baseline;
+ line-height: 1.6;
+}
+
+.file-last-change-side .change-id {
+ display: inline-block;
+ margin-bottom: 6px;
}
.file-last-change-msg {
color: var(--ink);
- flex: 1;
- white-space: nowrap;
+ margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.file-last-change-meta {
+ display: flex;
+ gap: 10px;
+ font-size: 11.5px;
}
.file-last-change-author {
@@ -780,6 +765,20 @@ html.dark {
color: var(--muted);
}
+/* Actions sidebar block */
+
+.file-actions-list {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ line-height: 1.8;
+}
+
+.file-action-row {
+ display: block;
+ color: var(--accent);
+ text-decoration: none;
+}
+
/* Code surface */
.code-surface {
@@ -788,7 +787,6 @@ html.dark {
line-height: 1.7;
display: grid;
grid-template-columns: auto 1fr;
- border-bottom: 1px solid var(--rule);
}
.code-gutter {
diff --git a/quire-server/templates/file.html b/quire-server/templates/file.html
index 91ed329..38fe437 100644
--- a/quire-server/templates/file.html
+++ b/quire-server/templates/file.html
@@ -20,60 +20,77 @@
</span>
</nav>
-{# ── Blob metadata + actions ─────────────────────────────────── #}
-<div class="file-meta">
- <span class="file-meta-item">
- <span class="file-meta-key">on</span>
- <span class="bookmark-glyph-sm">※</span>
- <a class="file-meta-link" href="/{{ repo }}/log">{{ bookmark }}</a>
- </span>
- <span class="repo-meta-dot">·</span>
- <span class="file-meta-item">
- <span class="file-meta-key">at</span>
- <a class="change-id" href="/{{ repo }}/log" title="commit {{ sha_short }}">
- <span class="change-head">{{ last_change_head }}</span><span class="change-tail">{{ last_change_tail }}</span>
- </a>
- </span>
- <span class="repo-meta-dot">·</span>
- <span class="file-meta-val">{{ line_count }} lines</span>
- <span class="repo-meta-dot">·</span>
- <span class="file-meta-val">{{ file_size }}</span>
- <span class="repo-meta-dot">·</span>
- <span class="file-meta-val lang-badge">{{ language }}</span>
- <span class="repo-meta-dot">·</span>
- <span class="file-meta-val">mode {{ mode }}</span>
- <span class="repo-meta-dot">·</span>
- <span class="file-meta-val">{{ encoding }}, {{ line_ending }}</span>
- <span class="file-meta-spacer"></span>
- <span class="file-actions">
- <a class="file-action-link" href="/{{ repo }}/raw/{{ path }}">raw</a>
- <a class="file-action-link file-action-link--muted" href="/{{ repo }}/blame/{{ path }}">blame</a>
- <a class="file-action-link file-action-link--muted" href="/{{ repo }}/log/{{ path }}">history</a>
- </span>
-</div>
-
-{# ── Last change strip ───────────────────────────────────────── #}
-<div class="file-last-change">
- <a class="change-id" href="/{{ repo }}/log" title="commit {{ last_change_sha }}">
- <span class="change-head">{{ last_change_head }}</span><span class="change-tail">{{ last_change_tail }}</span>
- </a>
- <span class="file-last-change-msg">{{ last_change_msg }}</span>
- <span class="file-last-change-author">{{ last_change_author }}</span>
- <span class="file-last-change-age">{{ last_change_age }}</span>
-</div>
+<div class="tree-body">
-{# ── Code surface — full width, gutter + content ─────────────── #}
-<div class="code-surface">
- <div class="code-gutter">
- {% for line_num in line_nums %}
- <div class="code-line-num">{{ line_num }}</div>
- {% endfor %}
- </div>
- <div class="code-body">
- {% for line in lines %}
- <div class="code-line"><pre>{{ line }}</pre></div>
- {% endfor %}
+ {# LEFT — code surface, full width within the column #}
+ <div class="file-code-col">
+ <div class="code-surface">
+ <div class="code-gutter">
+ {% for line_num in line_nums %}
+ <div class="code-line-num">{{ line_num }}</div>
+ {% endfor %}
+ </div>
+ <div class="code-body">
+ {% for line in lines %}
+ <div class="code-line"><pre>{{ line|safe }}</pre></div>
+ {% endfor %}
+ </div>
+ </div>
</div>
+
+ {# RIGHT — file info sidebar #}
+ <aside class="tree-sidebar">
+
+ <div class="side-block">
+ <div class="side-block-title">File</div>
+ <div class="file-info-list">
+ <div class="file-info-row">
+ <span class="file-info-key">lines</span>
+ <span class="file-info-val">{{ line_count }}</span>
+ </div>
+ <div class="file-info-row">
+ <span class="file-info-key">size</span>
+ <span class="file-info-val">{{ file_size }}</span>
+ </div>
+ <div class="file-info-row">
+ <span class="file-info-key">lang</span>
+ <span class="file-info-val">{{ language }}</span>
+ </div>
+ <div class="file-info-row">
+ <span class="file-info-key">mode</span>
+ <span class="file-info-val">{{ mode }}</span>
+ </div>
+ <div class="file-info-row">
+ <span class="file-info-key">encoding</span>
+ <span class="file-info-val">{{ encoding }}, {{ line_ending }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="side-block">
+ <div class="side-block-title">Last change</div>
+ <div class="file-last-change-side">
+ <a class="change-id" href="/{{ repo }}/log" title="commit {{ last_change_sha }}">
+ <span class="change-head">{{ last_change_head }}</span><span class="change-tail">{{ last_change_tail }}</span>
+ </a>
+ <div class="file-last-change-msg">{{ last_change_msg }}</div>
+ <div class="file-last-change-meta">
+ <span class="file-last-change-author">{{ last_change_author }}</span>
+ <span class="file-last-change-age">{{ last_change_age }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="side-block side-block--last">
+ <div class="side-block-title">Actions</div>
+ <div class="file-actions-list">
+ <a class="file-action-row" href="/{{ repo }}/raw/{{ path }}">raw</a>
+ <a class="file-action-row" href="/{{ repo }}/blame/{{ path }}">blame</a>
+ <a class="file-action-row" href="/{{ repo }}/log/{{ path }}">history</a>
+ </div>
+ </div>
+
+ </aside>
</div>
{% endblock %}