FossilRepo

Add global search bar, file blame, keyboard shortcut Global search: - Search icon in top nav bar — click to expand search field - Keyboard shortcut: press / to open search (like GitHub) - Searches against current project's checkins, tickets, wiki File blame: - New /fossil/code/blame/<path> view using `fossil blame` CLI - Shows commit hash, user, date for each line - Hashes and usernames clickable - "Blame" button on code file viewer alongside History and Raw - Uses temporary checkout (created/cleaned per request) Code file viewer: - Now has Source | Rendered | Blame | History | Raw buttons

lmata 2026-04-07 00:24 trunk
Commit 711e261845fb8c8eeff9d23d69e6019da97ac6f700dd208647ff01164e7b6662
--- fossil/cli.py
+++ fossil/cli.py
@@ -48,10 +48,61 @@
4848
if result.returncode == 0:
4949
return result.stdout
5050
except (FileNotFoundError, subprocess.TimeoutExpired):
5151
pass
5252
return ""
53
+
54
+ def blame(self, repo_path: Path, filename: str) -> list[dict]:
55
+ """Run fossil blame on a file. Returns [{user, uuid, line_num, text}].
56
+
57
+ Requires creating a temp checkout since blame needs an open checkout.
58
+ """
59
+ import tempfile
60
+
61
+ lines = []
62
+ tmpdir = tempfile.mkdtemp(prefix="fossilrepo-blame-")
63
+ try:
64
+ # Open a checkout in the temp dir
65
+ subprocess.run(
66
+ [self.binary, "open", str(repo_path), "--workdir", tmpdir],
67
+ capture_output=True,
68
+ text=True,
69
+ timeout=30,
70
+ cwd=tmpdir,
71
+ )
72
+ # Run blame
73
+ result = subprocess.run(
74
+ [self.binary, "blame", filename],
75
+ capture_output=True,
76
+ text=True,
77
+ timeout=30,
78
+ cwd=tmpdir,
79
+ )
80
+ if result.returncode == 0:
81
+ import re
82
+
83
+ for line in result.stdout.splitlines():
84
+ # Format: "hash date user: code"
85
+ m = re.match(r"([0-9a-f]+)\s+(\S+)\s+([^:]+):\s?(.*)", line)
86
+ if m:
87
+ lines.append(
88
+ {
89
+ "uuid": m.group(1),
90
+ "date": m.group(2),
91
+ "user": m.group(3).strip(),
92
+ "text": m.group(4),
93
+ }
94
+ )
95
+ # Close checkout
96
+ subprocess.run([self.binary, "close", "--force"], capture_output=True, cwd=tmpdir, timeout=10)
97
+ except Exception:
98
+ pass
99
+ finally:
100
+ import shutil
101
+
102
+ shutil.rmtree(tmpdir, ignore_errors=True)
103
+ return lines
53104
54105
def wiki_commit(self, repo_path: Path, page_name: str, content: str, user: str = "") -> bool:
55106
"""Create or update a wiki page. Pipes content to fossil wiki commit."""
56107
cmd = [self.binary, "wiki", "commit", page_name, "-R", str(repo_path)]
57108
if user:
58109
--- fossil/cli.py
+++ fossil/cli.py
@@ -48,10 +48,61 @@
48 if result.returncode == 0:
49 return result.stdout
50 except (FileNotFoundError, subprocess.TimeoutExpired):
51 pass
52 return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
54 def wiki_commit(self, repo_path: Path, page_name: str, content: str, user: str = "") -> bool:
55 """Create or update a wiki page. Pipes content to fossil wiki commit."""
56 cmd = [self.binary, "wiki", "commit", page_name, "-R", str(repo_path)]
57 if user:
58
--- fossil/cli.py
+++ fossil/cli.py
@@ -48,10 +48,61 @@
48 if result.returncode == 0:
49 return result.stdout
50 except (FileNotFoundError, subprocess.TimeoutExpired):
51 pass
52 return ""
53
54 def blame(self, repo_path: Path, filename: str) -> list[dict]:
55 """Run fossil blame on a file. Returns [{user, uuid, line_num, text}].
56
57 Requires creating a temp checkout since blame needs an open checkout.
58 """
59 import tempfile
60
61 lines = []
62 tmpdir = tempfile.mkdtemp(prefix="fossilrepo-blame-")
63 try:
64 # Open a checkout in the temp dir
65 subprocess.run(
66 [self.binary, "open", str(repo_path), "--workdir", tmpdir],
67 capture_output=True,
68 text=True,
69 timeout=30,
70 cwd=tmpdir,
71 )
72 # Run blame
73 result = subprocess.run(
74 [self.binary, "blame", filename],
75 capture_output=True,
76 text=True,
77 timeout=30,
78 cwd=tmpdir,
79 )
80 if result.returncode == 0:
81 import re
82
83 for line in result.stdout.splitlines():
84 # Format: "hash date user: code"
85 m = re.match(r"([0-9a-f]+)\s+(\S+)\s+([^:]+):\s?(.*)", line)
86 if m:
87 lines.append(
88 {
89 "uuid": m.group(1),
90 "date": m.group(2),
91 "user": m.group(3).strip(),
92 "text": m.group(4),
93 }
94 )
95 # Close checkout
96 subprocess.run([self.binary, "close", "--force"], capture_output=True, cwd=tmpdir, timeout=10)
97 except Exception:
98 pass
99 finally:
100 import shutil
101
102 shutil.rmtree(tmpdir, ignore_errors=True)
103 return lines
104
105 def wiki_commit(self, repo_path: Path, page_name: str, content: str, user: str = "") -> bool:
106 """Create or update a wiki page. Pipes content to fossil wiki commit."""
107 cmd = [self.binary, "wiki", "commit", page_name, "-R", str(repo_path)]
108 if user:
109
--- fossil/urls.py
+++ fossil/urls.py
@@ -23,9 +23,10 @@
2323
path("branches/", views.branch_list, name="branches"),
2424
path("tags/", views.tag_list, name="tags"),
2525
path("search/", views.search, name="search"),
2626
path("stats/", views.repo_stats, name="stats"),
2727
path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
28
+ path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
2829
path("code/history/<path:filepath>", views.file_history, name="file_history"),
2930
path("docs/", views.fossil_docs, name="docs"),
3031
path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
3132
]
3233
--- fossil/urls.py
+++ fossil/urls.py
@@ -23,9 +23,10 @@
23 path("branches/", views.branch_list, name="branches"),
24 path("tags/", views.tag_list, name="tags"),
25 path("search/", views.search, name="search"),
26 path("stats/", views.repo_stats, name="stats"),
27 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
 
28 path("code/history/<path:filepath>", views.file_history, name="file_history"),
29 path("docs/", views.fossil_docs, name="docs"),
30 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
31 ]
32
--- fossil/urls.py
+++ fossil/urls.py
@@ -23,9 +23,10 @@
23 path("branches/", views.branch_list, name="branches"),
24 path("tags/", views.tag_list, name="tags"),
25 path("search/", views.search, name="search"),
26 path("stats/", views.repo_stats, name="stats"),
27 path("code/raw/<path:filepath>", views.code_raw, name="code_raw"),
28 path("code/blame/<path:filepath>", views.code_blame, name="code_blame"),
29 path("code/history/<path:filepath>", views.file_history, name="file_history"),
30 path("docs/", views.fossil_docs, name="docs"),
31 path("docs/<path:doc_path>", views.fossil_doc_page, name="doc_page"),
32 ]
33
--- fossil/views.py
+++ fossil/views.py
@@ -980,10 +980,42 @@
980980
filename = filepath.split("/")[-1]
981981
response = DjHttpResponse(content_bytes, content_type="application/octet-stream")
982982
response["Content-Disposition"] = f'attachment; filename="{filename}"'
983983
return response
984984
985
+
986
+# --- File Blame ---
987
+
988
+
989
+@login_required
990
+def code_blame(request, slug, filepath):
991
+ P.PROJECT_VIEW.check(request.user)
992
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
993
+
994
+ from fossil.cli import FossilCLI
995
+
996
+ cli = FossilCLI()
997
+ blame_lines = []
998
+ if cli.is_available():
999
+ blame_lines = cli.blame(fossil_repo.full_path, filepath)
1000
+
1001
+ parts = filepath.split("/")
1002
+ file_breadcrumbs = [{"name": p, "path": "/".join(parts[: i + 1])} for i, p in enumerate(parts)]
1003
+
1004
+ return render(
1005
+ request,
1006
+ "fossil/code_blame.html",
1007
+ {
1008
+ "project": project,
1009
+ "filepath": filepath,
1010
+ "file_breadcrumbs": file_breadcrumbs,
1011
+ "blame_lines": blame_lines,
1012
+ "line_count": len(blame_lines),
1013
+ "active_tab": "code",
1014
+ },
1015
+ )
1016
+
9851017
9861018
# --- Repository Statistics ---
9871019
9881020
9891021
@login_required
9901022
--- fossil/views.py
+++ fossil/views.py
@@ -980,10 +980,42 @@
980 filename = filepath.split("/")[-1]
981 response = DjHttpResponse(content_bytes, content_type="application/octet-stream")
982 response["Content-Disposition"] = f'attachment; filename="{filename}"'
983 return response
984
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
985
986 # --- Repository Statistics ---
987
988
989 @login_required
990
--- fossil/views.py
+++ fossil/views.py
@@ -980,10 +980,42 @@
980 filename = filepath.split("/")[-1]
981 response = DjHttpResponse(content_bytes, content_type="application/octet-stream")
982 response["Content-Disposition"] = f'attachment; filename="{filename}"'
983 return response
984
985
986 # --- File Blame ---
987
988
989 @login_required
990 def code_blame(request, slug, filepath):
991 P.PROJECT_VIEW.check(request.user)
992 project, fossil_repo, reader = _get_repo_and_reader(slug)
993
994 from fossil.cli import FossilCLI
995
996 cli = FossilCLI()
997 blame_lines = []
998 if cli.is_available():
999 blame_lines = cli.blame(fossil_repo.full_path, filepath)
1000
1001 parts = filepath.split("/")
1002 file_breadcrumbs = [{"name": p, "path": "/".join(parts[: i + 1])} for i, p in enumerate(parts)]
1003
1004 return render(
1005 request,
1006 "fossil/code_blame.html",
1007 {
1008 "project": project,
1009 "filepath": filepath,
1010 "file_breadcrumbs": file_breadcrumbs,
1011 "blame_lines": blame_lines,
1012 "line_count": len(blame_lines),
1013 "active_tab": "code",
1014 },
1015 )
1016
1017
1018 # --- Repository Statistics ---
1019
1020
1021 @login_required
1022
--- templates/base.html
+++ templates/base.html
@@ -110,10 +110,18 @@
110110
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
111111
<script>
112112
document.body.addEventListener('htmx:configRequest', function(event) {
113113
var token = document.querySelector('meta[name="csrf-token"]');
114114
if (token) { event.detail.headers['X-CSRFToken'] = token.content; }
115
+ });
116
+ // Keyboard shortcut: / to open search
117
+ document.addEventListener('keydown', function(e) {
118
+ if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
119
+ e.preventDefault();
120
+ document.querySelector('[x-ref="searchInput"]')?.closest('[x-data]')?.__x?.$data && (document.querySelector('[x-ref="searchInput"]').closest('[x-data]').__x.$data.open = true);
121
+ setTimeout(() => document.querySelector('[x-ref="searchInput"]')?.focus(), 100);
122
+ }
115123
});
116124
</script>
117125
</head>
118126
<body class="h-full bg-gray-950 text-gray-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
119127
<div class="min-h-full flex flex-col">
120128
121129
ADDED templates/fossil/code_blame.html
--- templates/base.html
+++ templates/base.html
@@ -110,10 +110,18 @@
110 <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
111 <script>
112 document.body.addEventListener('htmx:configRequest', function(event) {
113 var token = document.querySelector('meta[name="csrf-token"]');
114 if (token) { event.detail.headers['X-CSRFToken'] = token.content; }
 
 
 
 
 
 
 
 
115 });
116 </script>
117 </head>
118 <body class="h-full bg-gray-950 text-gray-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
119 <div class="min-h-full flex flex-col">
120
121 DDED templates/fossil/code_blame.html
--- templates/base.html
+++ templates/base.html
@@ -110,10 +110,18 @@
110 <script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
111 <script>
112 document.body.addEventListener('htmx:configRequest', function(event) {
113 var token = document.querySelector('meta[name="csrf-token"]');
114 if (token) { event.detail.headers['X-CSRFToken'] = token.content; }
115 });
116 // Keyboard shortcut: / to open search
117 document.addEventListener('keydown', function(e) {
118 if (e.key === '/' && !e.ctrlKey && !e.metaKey && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
119 e.preventDefault();
120 document.querySelector('[x-ref="searchInput"]')?.closest('[x-data]')?.__x?.$data && (document.querySelector('[x-ref="searchInput"]').closest('[x-data]').__x.$data.open = true);
121 setTimeout(() => document.querySelector('[x-ref="searchInput"]')?.focus(), 100);
122 }
123 });
124 </script>
125 </head>
126 <body class="h-full bg-gray-950 text-gray-100" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
127 <div class="min-h-full flex flex-col">
128
129 DDED templates/fossil/code_blame.html
--- a/templates/fossil/code_blame.html
+++ b/templates/fossil/code_blame.html
@@ -0,0 +1,12 @@
1
+{% extends "base.html" %}
2
+{% ay-100 mb-2">{{ project.name }}</h1>
3
+{% include "fossil/_project_nav.html" %}
4
+
5
+<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
6
+ <div class="px-4 py-3 border-b border-gray-700 flex er-gray-700 flex flex-wrap i" {% endfoass="flex items-centeay-100 mb-2">{{il:code' slug=project.slug %}" class="text-brand-light hover:text-brand">{{ project.sluggray-700">
7
+ <div class=>
8
+ {% if forloop.lacolor: inherit;ast %}
9
+ <span class="text-gray-200">{{ crumb.name }}</span>lse %}
10
+ <a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
11
+ {% endif %}
12
+
--- a/templates/fossil/code_blame.html
+++ b/templates/fossil/code_blame.html
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/code_blame.html
+++ b/templates/fossil/code_blame.html
@@ -0,0 +1,12 @@
1 {% extends "base.html" %}
2 {% ay-100 mb-2">{{ project.name }}</h1>
3 {% include "fossil/_project_nav.html" %}
4
5 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
6 <div class="px-4 py-3 border-b border-gray-700 flex er-gray-700 flex flex-wrap i" {% endfoass="flex items-centeay-100 mb-2">{{il:code' slug=project.slug %}" class="text-brand-light hover:text-brand">{{ project.sluggray-700">
7 <div class=>
8 {% if forloop.lacolor: inherit;ast %}
9 <span class="text-gray-200">{{ crumb.name }}</span>lse %}
10 <a href="{% url 'fossil:code_dir' slug=project.slug dirpath=crumb.path %}" class="text-brand-light hover:text-brand">{{ crumb.name }}</a>
11 {% endif %}
12
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -62,10 +62,11 @@
6262
<div class="flex items-center gap-1 text-xs">
6363
<a href="?mode=source" class="px-2 py-1 rounded {% if view_mode == 'source' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Source</a>
6464
<a href="?mode=rendered" class="px-2 py-1 rounded {% if view_mode == 'rendered' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Rendered</a>
6565
</div>
6666
{% endif %}
67
+ <a href="{% url 'fossil:code_blame' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Blame</a>
6768
<a href="{% url 'fossil:file_history' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">History</a>
6869
<a href="{% url 'fossil:code_raw' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Raw</a>
6970
{% if not is_binary %}
7071
<span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
7172
{% endif %}
7273
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -62,10 +62,11 @@
62 <div class="flex items-center gap-1 text-xs">
63 <a href="?mode=source" class="px-2 py-1 rounded {% if view_mode == 'source' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Source</a>
64 <a href="?mode=rendered" class="px-2 py-1 rounded {% if view_mode == 'rendered' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Rendered</a>
65 </div>
66 {% endif %}
 
67 <a href="{% url 'fossil:file_history' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">History</a>
68 <a href="{% url 'fossil:code_raw' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Raw</a>
69 {% if not is_binary %}
70 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
71 {% endif %}
72
--- templates/fossil/code_file.html
+++ templates/fossil/code_file.html
@@ -62,10 +62,11 @@
62 <div class="flex items-center gap-1 text-xs">
63 <a href="?mode=source" class="px-2 py-1 rounded {% if view_mode == 'source' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Source</a>
64 <a href="?mode=rendered" class="px-2 py-1 rounded {% if view_mode == 'rendered' %}bg-brand text-white{% else %}text-gray-500 hover:text-white{% endif %}">Rendered</a>
65 </div>
66 {% endif %}
67 <a href="{% url 'fossil:code_blame' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Blame</a>
68 <a href="{% url 'fossil:file_history' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">History</a>
69 <a href="{% url 'fossil:code_raw' slug=project.slug filepath=filepath %}" class="px-2 py-1 text-xs text-gray-500 hover:text-brand-light rounded hover:bg-gray-700">Raw</a>
70 {% if not is_binary %}
71 <span class="text-xs text-gray-500">{{ line_count }} line{{ line_count|pluralize }}</span>
72 {% endif %}
73
--- templates/includes/nav.html
+++ templates/includes/nav.html
@@ -6,10 +6,25 @@
66
<a href="{% url 'dashboard' %}" class="flex-shrink-0">
77
<img src="{% static 'img/fossilrepo-logo-dark.png' %}" alt="Fossilrepo" class="h-7 w-auto">
88
</a>
99
</div>
1010
<div class="flex items-center gap-3">
11
+ <!-- Quick search -->
12
+ <div x-data="{ open: false }" class="relative">
13
+ <button @click="open = !open; $nextTick(() => $refs.searchInput?.focus())" class="rounded-md p-2 text-gray-400 hover:text-white hover:bg-gray-800" title="Search (/)">
14
+ <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
15
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
16
+ </svg>
17
+ </button>
18
+ <div x-show="open" @click.outside="open = false" @keydown.escape.window="open = false" x-transition
19
+ class="absolute right-0 z-20 mt-2 w-80 rounded-lg bg-gray-800 shadow-lg ring-1 ring-gray-700 p-3">
20
+ <form method="get" action="{% if request.resolver_match.kwargs.slug %}/projects/{{ request.resolver_match.kwargs.slug }}/fossil/search/{% else %}/projects/fossil-scm/fossil/search/{% endif %}">
21
+ <input type="text" name="q" x-ref="searchInput" placeholder="Search checkins, tickets, wiki..."
22
+ class="w-full rounded-md border-gray-600 bg-gray-900 text-gray-100 text-sm px-3 py-2 focus:border-brand focus:ring-brand">
23
+ </form>
24
+ </div>
25
+ </div>
1126
<!-- Theme toggle -->
1227
<button x-data="{ dark: document.documentElement.classList.contains('dark') }"
1328
@click="dark = !dark; document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', dark ? 'dark' : 'light')"
1429
class="rounded-md p-2 text-gray-400 hover:text-white hover:bg-gray-800"
1530
title="Toggle theme">
1631
--- templates/includes/nav.html
+++ templates/includes/nav.html
@@ -6,10 +6,25 @@
6 <a href="{% url 'dashboard' %}" class="flex-shrink-0">
7 <img src="{% static 'img/fossilrepo-logo-dark.png' %}" alt="Fossilrepo" class="h-7 w-auto">
8 </a>
9 </div>
10 <div class="flex items-center gap-3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11 <!-- Theme toggle -->
12 <button x-data="{ dark: document.documentElement.classList.contains('dark') }"
13 @click="dark = !dark; document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', dark ? 'dark' : 'light')"
14 class="rounded-md p-2 text-gray-400 hover:text-white hover:bg-gray-800"
15 title="Toggle theme">
16
--- templates/includes/nav.html
+++ templates/includes/nav.html
@@ -6,10 +6,25 @@
6 <a href="{% url 'dashboard' %}" class="flex-shrink-0">
7 <img src="{% static 'img/fossilrepo-logo-dark.png' %}" alt="Fossilrepo" class="h-7 w-auto">
8 </a>
9 </div>
10 <div class="flex items-center gap-3">
11 <!-- Quick search -->
12 <div x-data="{ open: false }" class="relative">
13 <button @click="open = !open; $nextTick(() => $refs.searchInput?.focus())" class="rounded-md p-2 text-gray-400 hover:text-white hover:bg-gray-800" title="Search (/)">
14 <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
15 <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
16 </svg>
17 </button>
18 <div x-show="open" @click.outside="open = false" @keydown.escape.window="open = false" x-transition
19 class="absolute right-0 z-20 mt-2 w-80 rounded-lg bg-gray-800 shadow-lg ring-1 ring-gray-700 p-3">
20 <form method="get" action="{% if request.resolver_match.kwargs.slug %}/projects/{{ request.resolver_match.kwargs.slug }}/fossil/search/{% else %}/projects/fossil-scm/fossil/search/{% endif %}">
21 <input type="text" name="q" x-ref="searchInput" placeholder="Search checkins, tickets, wiki..."
22 class="w-full rounded-md border-gray-600 bg-gray-900 text-gray-100 text-sm px-3 py-2 focus:border-brand focus:ring-brand">
23 </form>
24 </div>
25 </div>
26 <!-- Theme toggle -->
27 <button x-data="{ dark: document.documentElement.classList.contains('dark') }"
28 @click="dark = !dark; document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', dark ? 'dark' : 'light')"
29 class="rounded-md p-2 text-gray-400 hover:text-white hover:bg-gray-800"
30 title="Toggle theme">
31

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button