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
change ootnkxyumkpzokvxwpztzuproqsynuqk
commit 8e32243a5c6a1fae5f50dd15786f7973c6435c76
author Alpha Chen <alpha@kejadlen.dev>
date
parent oynymvll
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('>', "&gt;")
         .replace('"', "&quot;")
 }
+
+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 %}