Compare commits

...

2 Commits

Author SHA1 Message Date
Codex 3a3fd97055 Split TUI from default watch mode 2026-03-30 11:05:44 +08:00
sxlxc aa05d73c9c Clean up CSS and update default styles
Normalize formatting across CSS files and fix selector whitespace
and quote usage. Change default link color to browser blue and
enable smooth scrolling; hover underlines links. Revamp table
styles (borders, caption, zebra rows, padding, footer alignment).
Adjust proof/theorem header spacing and minor spacing/padding fixes.
2026-03-30 10:42:25 +08:00
5 changed files with 352 additions and 290 deletions
+51 -38
View File
@@ -1,90 +1,103 @@
.theorem-environment {
font-style: italic;
margin-top: 1em;
padding: 0.5em;
background-color: whitesmoke;
font-style: italic;
margin-top: 1em;
padding: 0.5em;
background-color: whitesmoke;
}
.theorem-header {
font-weight: bold;
font-style: normal;
font-weight: bold;
font-style: normal;
}
.theorem-header .index:before {
content: ' ';
content: " ";
}
.theorem-header .name:before {
content: ' (';
content: " (";
}
.theorem-header .name:after {
content: ')';
content: ")";
}
.theorem-header:after {
content: '.\2002\2002';
content: ".\2002\2002";
}
.theorem-header+p {
display: inline;
.theorem-header + p {
display: inline;
}
.Proof .type {
font-style: italic;
font-weight: normal;
font-style: italic;
font-weight: normal;
}
.Proof {
background: none;
font-style: normal;
position: relative;
background: none;
font-style: normal;
position: relative;
}
.Proof:after {
content: '∎';
position: absolute;
right: 0px;
bottom: 0px;
content: "∎";
position: absolute;
right: 0px;
bottom: 0px;
}
.Proof span.theorem-header span.name {
font-weight: normal;
font-style: italic;
}
.Proof span.theorem-header span.name:before {
content: " ";
}
.Proof span.theorem-header span.name:after {
content: " ";
}
table.postindex {
width: 100%;
width: 100%;
}
table.postindex cite {
font-style: normal;
font-style: normal;
}
table.postindex td.right {
text-align: right;
width: 11ex;
text-align: right;
width: 11ex;
}
.header-section-number {
margin-right: 10px;
margin-right: 10px;
}
.header-section-number:after {
content: '.';
content: ".";
}
.csl-entry {
display: table;
width: 100%;
table-layout: auto;
display: table;
width: 100%;
table-layout: auto;
}
.csl-left-margin {
display: table-cell;
padding-right: 0.5em;
white-space: nowrap;
width: 1px;
display: table-cell;
padding-right: 0.5em;
white-space: nowrap;
width: 1px;
}
.csl-right-inline {
display: table-cell;
display: table-cell;
}
.csl-right-inline a {
word-break: break-all;
}
.csl-right-inline a{
word-break: break-all;
}
+66 -119
View File
@@ -3,10 +3,7 @@
--color-tag1: gray;
--color-tag2: darkolivegreen;
--color-bg: white;
--color-link: #337ab7;
--color-linkhbg: #e6f0ff;
--color-linkh: #002266;
--color-bq: olivedrab;
--color-link: #0000ee;
--color-notice: #fb4f4f;
}
@@ -19,11 +16,18 @@
html {
scrollbar-gutter: stable;
scroll-behavior: smooth;
font-size: 14pt;
}
body {
font-family: 'Lato', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-family:
"Lato",
-apple-system,
BlinkMacSystemFont,
"PingFang SC",
"Microsoft YaHei",
sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
@@ -32,30 +36,28 @@ body {
color: var(--color-text);
background-color: var(--color-bg);
text-rendering: optimizeLegibility;
}
body.lang-zh {
text-align: left;
text-autospace: no-autospace; /*using pangu.hs*/
}
body a {
color: var(--color-link);
text-decoration: none;
}
.text-space a:hover {
background-color: var(--color-linkhbg);
color: var(--color-linkh);
text-decoration: none;
body a:hover {
text-decoration: underline;
}
details {
background-color: var(--color-linkhbg);
padding-left: 1em;
border: 2px solid var(--color-text);
}
summary:hover {
cursor: pointer;
}
/*mathML*/
.htmlmathparagraph, mtext,math {
.htmlmathparagraph,
mtext,
math {
font-family: Lete Sans Math;
}
.math-container,
@@ -63,7 +65,7 @@ summary:hover {
display: block;
overflow-x: auto;
overflow-y: hidden;
padding: .5em;
padding: 0.5em;
}
.math-container.math-container-tagged {
@@ -72,7 +74,7 @@ summary:hover {
align-items: center;
column-gap: 1rem;
overflow: visible;
padding: .5em 0;
padding: 0.5em 0;
}
.math-container.math-container-tagged .math-tag-spacer {
@@ -83,7 +85,7 @@ summary:hover {
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
padding: .5em 0;
padding: 0.5em 0;
}
.math-container.math-container-tagged .math-tag {
@@ -104,66 +106,32 @@ summary:hover {
font-variant-caps: small-caps;
}
p {
hyphens: auto;
}
a.url {
word-break: break-all;
}
html body div.text-space main ul.post-list {
list-style-type: none;
padding-left: 1em;
}
/* top bar */
header {
font-weight: 400;
font-family: "IosevkaC", sans-serif;
}
/* top bar*/
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.navright a {
margin: 0 0 0 1em;
}
/* Links inside the navbar */
.navbar a {
text-decoration: none;
color: var(--color-text);
}
.navbar a:visited {
color: var(--color-text);
}
nav {
text-align: right;
border-bottom: solid 1px var(--color-text);
}
nav a {
font-size: 1.2rem;
/*margin-left: 0.5em;*/
display: inline-block;
vertical-align: middle;
text-decoration: none;
}
.uri {
word-wrap: break-word;
/* Legacy support */
overflow-wrap: break-word;
/* Modern property */
word-break: break-all;
/* Break long words if necessary */
white-space: normal;
/* Allow wrapping */
}
footer {
color: var(--color-text);
font-size: 0.8rem;
@@ -172,15 +140,6 @@ footer {
padding-right: 1em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-align: left;
}
.pagetitle {
font-size: 2rem;
font-weight: normal;
@@ -194,22 +153,20 @@ h1 {
font-size: 1.44rem;
font-weight: bold;
font-style: normal;
text-align: left;
line-height: 100%;
}
h2 {
margin-top: 1em;
font-size: 1.2rem;
font-weight: bold;
font-style: normal
font-style: normal;
}
h3 {
margin-top: 1em;
font-size: 1rem;
font-weight: bold;
font-style: normal
font-style: normal;
}
article .header {
@@ -219,14 +176,7 @@ article .header {
text-align: left;
}
.info {
color: var(--color-tag2);
font-size: 1rem;
font-style: normal;
text-align: left;
}
.info,
.info a {
color: var(--color-tag2);
font-size: 1rem;
@@ -247,33 +197,45 @@ section.body {
line-height: normal;
}
blockquote {
margin: 1rem 0;
padding: 0 0 0 1.5em;
border-left: 3px solid var(--color-bq);
/* table. copied from https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table */
table {
border-collapse: collapse;
border: 2px solid rgb(140 140 140);
font-size: 0.8rem;
letter-spacing: 1px;
}
blockquote p {
margin: 0;
caption {
caption-side: bottom;
padding: 10px;
font-weight: bold;
}
ol {
padding-left: 2em;
}
ul {
list-style-type: square;
padding-left: 2em;
}
li {
margin-bottom: 0.15em;
thead,
tfoot {
background-color: rgb(228 240 245);
}
table,
th,
td {
border: 1px solid darkolivegreen;
border-collapse: collapse;
text-align: left;
border: 1px solid rgb(160 160 160);
padding: 8px 10px;
}
td:last-of-type {
text-align: center;
}
tbody > tr:nth-of-type(even) {
background-color: rgb(237 238 242);
}
tfoot th {
text-align: right;
}
tfoot td {
font-weight: bold;
}
figure {
@@ -284,23 +246,11 @@ figure {
max-width: 80%;
}
figcaption {
/* font: italic smaller sans-serif; */
padding: 3px;
text-align: center;
}
.caption {
display: none
}
.centerimg img {
margin: 0 auto 0 auto;
display: block;
}
div.highlight,
pre code {
margin: auto;
@@ -320,18 +270,16 @@ code {
text-rendering: optimizeSpeed;
}
.draft-notice {
color: var(--color-notice);
margin: 1em auto;
text-align: center
text-align: center;
}
.subtitle {
text-align: left;
font-size: 1.2rem;
margin-top: 0
margin-top: 0;
}
.gallery {
margin-top: 2em;
@@ -401,7 +349,7 @@ code {
padding-left: 1em;
line-height: 1.2;
list-style-type: decimal;
margin-left: 0
margin-left: 0;
}
div#contents ul.notes-list,
@@ -414,8 +362,8 @@ code {
list-style-type: none;
}
div#contents-big li+li {
margin-top: 0.5em
div#contents-big li + li {
margin-top: 0.5em;
}
div#contents-big {
@@ -428,7 +376,7 @@ code {
margin-right: 4em;
position: sticky;
top: 5rem;
left: 100%
left: 100%;
}
div#contents-big .mini-header {
@@ -452,7 +400,6 @@ code {
}
@media print {
.no-print,
.no-print * {
display: none !important;
+33 -34
View File
@@ -1,52 +1,51 @@
/* fonts */
@font-face {
font-family: "Lato";
src: url("/fonts/Lato-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-family: "Lato";
src: url("/fonts/Lato-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Lato";
src: url("/fonts/Lato-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
font-family: "Lato";
src: url("/fonts/Lato-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "Lato";
src: url("/fonts/Lato-Italic.woff2") format("woff2");
font-weight: normal;
font-style: italic;
font-family: "Lato";
src: url("/fonts/Lato-Italic.woff2") format("woff2");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "Lato";
src: url("/fonts/Lato-BoldItalic.woff2") format("woff2");
font-weight: bold;
font-style: italic;
font-family: "Lato";
src: url("/fonts/Lato-BoldItalic.woff2") format("woff2");
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: "Lete Sans Math";
src: url("/fonts/LeteSansMath.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-family: "Lete Sans Math";
src: url("/fonts/LeteSansMath.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Lete Sans Math";
src: url("/fonts/LeteSansMath-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
font-family: "Lete Sans Math";
src: url("/fonts/LeteSansMath-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "IosevkaC";
src: url("/fonts/IosevkaCustom-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-family: "IosevkaC";
src: url("/fonts/IosevkaCustom-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "IosevkaC";
src: url("/fonts/IosevkaCustom-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
font-family: "IosevkaC";
src: url("/fonts/IosevkaCustom-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
}
+20 -7
View File
@@ -54,7 +54,7 @@ cabal install exe:hakysidian
## Commands
The CLI mirrors the common Hakyll workflow:
The default CLI mirrors the common Hakyll workflow:
```bash
hakysidian build
@@ -70,28 +70,41 @@ hakysidian watch --host 127.0.0.1 --port 8000
hakysidian watch --no-server
```
The dashboard is now an explicit TUI mode:
```bash
hakysidian -tui
hakysidian -tui --host 127.0.0.1 --port 8000
hakysidian -tui --no-server
```
What each command does:
- `build`: incremental site build.
- `clean`: removes generated output and cache.
- `rebuild`: clears output/cache and builds from scratch.
- `watch`: shows an in-place terminal dashboard, watches project inputs, and rebuilds automatically on change.
- `watch`: runs Hakyll's normal watch workflow, prints build logs directly to the terminal, and rebuilds automatically on change.
- `-tui`: starts the interactive dashboard with explicit controls for watching and cleaning.
## Watch Mode
## Watch And TUI
`watch` tracks:
Both `watch` and `-tui` work against the same project inputs:
- `notes/**`
- `reference.bib`
- `math-macros.md`
- `images/**`
The watch UI:
Normal `watch` behaves like a standard Hakyll watch command: it stays in the terminal, rebuilds when inputs change, and can start a preview server unless `--no-server` is passed.
`-tui` uses an alternate-screen dashboard that:
- uses the terminals current size to keep the dashboard within the visible screen,
- keeps recent build output in a bounded activity pane,
- avoids scrolling raw Hakyll logs through the terminal,
- can start a local preview server unless `--no-server` is passed.
- can start a local preview server unless `--no-server` is passed,
- supports `w` to start watching, `s` to stop watching, `c` to clean, and `q` to quit.
The TUI requires an interactive terminal.
## Notes Format
+182 -92
View File
@@ -9,7 +9,7 @@ import ChaoDoc
import Control.Concurrent (forkIO, threadDelay)
import Control.Exception (SomeException, bracket_, try)
import Control.Monad (filterM, unless, void, when)
import Data.Char (isSpace)
import Data.Char (isSpace, toLower)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Kind (Type)
import Data.List (intercalate, isPrefixOf, sort, sortOn)
@@ -112,11 +112,24 @@ data CliCommand
| CleanCommand
| HelpCommand
| RebuildCommand
| TuiCommand WatchSettings
| WatchCommand WatchSettings
type TuiAction :: Type
data TuiAction
= TuiClean
| TuiQuit
| TuiStartWatching
| TuiStopWatching
type FileSnapshot :: Type
type FileSnapshot = M.Map FilePath UTCTime
type TuiWatchState :: Type
data TuiWatchState
= TuiWatchStopped
| TuiWatching FileSnapshot
type ServerStatus :: Type
data ServerStatus
= ServerDisabled
@@ -203,14 +216,17 @@ main = do
Right RebuildCommand -> do
validateProject projectRoot
exitWith =<< runSiteCommand config rebuildOptions cslPath
Right (TuiCommand watchSettings) -> do
validateProject projectRoot
exitWith =<< runTui projectRoot config cslPath watchSettings
Right (WatchCommand watchSettings) -> do
validateProject projectRoot
exitWith =<< runWatch projectRoot config cslPath watchSettings
exitWith =<< runSiteCommand config (watchOptions watchSettings) cslPath
usageText :: String
usageText =
unlines
[ "usage: hakysidian [build|clean|rebuild|watch [--host HOST] [--port PORT] [--no-server]]",
[ "usage: hakysidian [build|clean|rebuild|watch [--host HOST] [--port PORT] [--no-server]|-tui [--host HOST] [--port PORT] [--no-server]]",
"",
"Run inside a project directory containing notes/, reference.bib, math-macros.md, and optional images/."
]
@@ -223,6 +239,8 @@ parseCliCommand config args
["build"] -> Right BuildCommand
["clean"] -> Right CleanCommand
["rebuild"] -> Right RebuildCommand
"-tui" : rest -> Right (TuiCommand (parseWatchSettings config rest))
"--tui" : rest -> Right (TuiCommand (parseWatchSettings config rest))
"watch" : rest -> Right (WatchCommand (parseWatchSettings config rest))
command : _ -> Left ("Unknown command: " <> command)
@@ -251,12 +269,12 @@ validateProject projectRoot = do
initialDashboardState :: DashboardState
initialDashboardState =
DashboardState
{ dashboardStatus = "starting",
dashboardLastChange = "waiting for first build",
dashboardLastBuild = "pending",
{ dashboardStatus = "idle",
dashboardLastChange = "press w to start watching",
dashboardLastBuild = "no command run yet",
dashboardLogLines =
[ "watcher ready",
"watching notes/, reference.bib, math-macros.md, images/ (optional)"
[ "tui ready",
"controls: w watch, s stop, c clean, q quit"
]
}
@@ -353,12 +371,11 @@ watchInputMode inputMode =
renderWatchDashboard ::
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
FilePath ->
Configuration ->
WatchSettings ->
IORef ServerStatus ->
DashboardState ->
IO ()
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard = do
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef dashboard = do
terminalSize <- getTerminalSize
serverStatus <- readIORef serverStatusRef
previousRenderState <- readIORef renderStateRef
@@ -369,22 +386,20 @@ renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatu
border = "+" ++ replicate (cols - 2) '-' ++ "+"
infoRows =
[ dashboardRow cols ("Project : " ++ projectRoot),
-- dashboardRow cols ("Output : " ++ destinationDirectory config),
dashboardRow cols ("Preview : " ++ renderServerStatus watchSettings serverStatus),
-- dashboardRow cols "Watch : notes/, reference.bib, math-macros.md, images/ (optional)",
-- dashboardRow cols ("Change : " ++ dashboardLastChange dashboard),
dashboardRow cols ("Build : " ++ dashboardLastBuild dashboard)
dashboardRow cols ("Change : " ++ dashboardLastChange dashboard),
dashboardRow cols ("Last op : " ++ dashboardLastBuild dashboard)
]
headerRows =
[ border,
dashboardTitleRow cols "hakysidian watch" (dashboardStatus dashboard),
dashboardTitleRow cols "hakysidian tui" (dashboardStatus dashboard),
border
]
++ infoRows
++ [border, dashboardRow cols "Recent activity", border]
footerRows =
[ border,
dashboardRow cols "Controls: q quit, Ctrl-C interrupt",
dashboardRow cols "Controls: w watch, s stop, c clean, q quit, Ctrl-C interrupt",
border
]
availableLogRows = max 1 (rows - length headerRows - length footerRows)
@@ -483,20 +498,20 @@ watchLoopDelayMicros = 1000000
watchInputPollMicros :: Int
watchInputPollMicros = 100000
waitForWatchQuit :: Bool -> Int -> IO Bool
waitForWatchQuit watchInputEnabled remainingMicros
| remainingMicros <= 0 = pure False
waitForTuiAction :: Bool -> Int -> IO (Maybe TuiAction)
waitForTuiAction watchInputEnabled remainingMicros
| remainingMicros <= 0 = pure Nothing
| otherwise = do
shouldQuit <- pollWatchQuit watchInputEnabled
if shouldQuit
then pure True
else do
nextAction <- pollTuiAction watchInputEnabled
case nextAction of
Just action -> pure (Just action)
Nothing -> do
threadDelay (min watchInputPollMicros remainingMicros)
waitForWatchQuit watchInputEnabled (remainingMicros - watchInputPollMicros)
waitForTuiAction watchInputEnabled (remainingMicros - watchInputPollMicros)
pollWatchQuit :: Bool -> IO Bool
pollWatchQuit watchInputEnabled
| not watchInputEnabled = pure False
pollTuiAction :: Bool -> IO (Maybe TuiAction)
pollTuiAction watchInputEnabled
| not watchInputEnabled = pure Nothing
| otherwise = drainInput
where
drainInput = do
@@ -504,10 +519,18 @@ pollWatchQuit watchInputEnabled
if hasInput
then do
inputChar <- hGetChar stdin
if inputChar == 'q'
then pure True
else drainInput
else pure False
case parseTuiAction inputChar of
Just action -> pure (Just action)
Nothing -> drainInput
else pure Nothing
parseTuiAction :: Char -> Maybe TuiAction
parseTuiAction inputChar = case toLower inputChar of
'c' -> Just TuiClean
'q' -> Just TuiQuit
's' -> Just TuiStopWatching
'w' -> Just TuiStartWatching
_ -> Nothing
trimTrailingSpace :: String -> String
trimTrailingSpace = reverse . dropWhile isSpace . reverse
@@ -546,109 +569,176 @@ cleanOptions = Options {verbosity = False, optCommand = Clean}
rebuildOptions :: Options
rebuildOptions = Options {verbosity = False, optCommand = Rebuild}
watchOptions :: WatchSettings -> Options
watchOptions watchSettings =
Options
{ verbosity = False,
optCommand =
Watch
{ host = watchHost watchSettings,
port = watchPort watchSettings,
no_server = not (watchServerEnabled watchSettings)
}
}
runSiteCommand :: Configuration -> Options -> FilePath -> IO ExitCode
runSiteCommand config options cslPath =
hakyllWithExitCodeAndArgs config options (siteRules cslPath)
runWatch :: FilePath -> Configuration -> FilePath -> WatchSettings -> IO ExitCode
runWatch projectRoot config _cslPath watchSettings = do
runTui :: FilePath -> Configuration -> FilePath -> WatchSettings -> IO ExitCode
runTui projectRoot config _cslPath watchSettings = do
stdoutInteractive <- hIsTerminalDevice stdout
stdinInteractive <- hIsTerminalDevice stdin
let watchInputEnabled = stdoutInteractive && stdinInteractive
withWatchTui do
serverStatusRef <- newIORef initialServerStatus
renderStateRef <- newIORef Nothing
startPreviewServer config watchSettings serverStatusRef
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef initialDashboardState
(_, initialDashboard) <-
runWatchBuild
"build"
"initial build"
"initial build"
projectRoot
config
watchSettings
renderStateRef
serverStatusRef
initialDashboardState
initialSnapshot <- snapshotInputs projectRoot
watchLoop watchInputEnabled renderStateRef serverStatusRef initialSnapshot initialDashboard
if watchInputEnabled
then
withWatchTui do
serverStatusRef <- newIORef initialServerStatus
renderStateRef <- newIORef Nothing
startPreviewServer config watchSettings serverStatusRef
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef initialDashboardState
tuiLoop watchInputEnabled renderStateRef serverStatusRef TuiWatchStopped initialDashboardState
else do
putStrLn "hakysidian -tui requires an interactive terminal."
pure (ExitFailure 1)
where
initialServerStatus
| watchServerEnabled watchSettings = ServerStarting
| otherwise = ServerDisabled
watchLoop ::
tuiLoop ::
Bool ->
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
IORef ServerStatus ->
FileSnapshot ->
TuiWatchState ->
DashboardState ->
IO ExitCode
watchLoop watchInputEnabled renderStateRef serverStatusRef previousSnapshot dashboard = do
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef dashboard
shouldQuit <- waitForWatchQuit watchInputEnabled watchLoopDelayMicros
if shouldQuit
then pure ExitSuccess
else do
nextSnapshot <- snapshotInputs projectRoot
if nextSnapshot == previousSnapshot
then watchLoop watchInputEnabled renderStateRef serverStatusRef previousSnapshot dashboard
else do
let changedFiles = diffSnapshots previousSnapshot nextSnapshot
command :: String
command =
if any (`M.notMember` nextSnapshot) (M.keys previousSnapshot)
then "rebuild"
else "build"
changeSummary = intercalate ", " changedFiles
(_, nextDashboard) <-
runWatchBuild
command
command
changeSummary
projectRoot
config
watchSettings
renderStateRef
serverStatusRef
dashboard
watchLoop watchInputEnabled renderStateRef serverStatusRef nextSnapshot nextDashboard
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard = do
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef dashboard
nextAction <- waitForTuiAction watchInputEnabled watchLoopDelayMicros
case nextAction of
Just TuiQuit -> pure ExitSuccess
Just TuiStartWatching -> case watchState of
TuiWatching _ ->
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
TuiWatchStopped -> do
(_, nextDashboard) <-
runDashboardCommand
"rebuild"
"watch start"
"manual start"
"building (watch start)"
(watchCommandStatus "watch start")
projectRoot
watchSettings
renderStateRef
serverStatusRef
dashboard
nextSnapshot <- snapshotInputs projectRoot
tuiLoop watchInputEnabled renderStateRef serverStatusRef (TuiWatching nextSnapshot) nextDashboard
Just TuiStopWatching -> case watchState of
TuiWatchStopped ->
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
TuiWatching _ -> do
nextDashboard <-
appendDashboardMessage
( dashboard
{ dashboardStatus = "idle",
dashboardLastChange = "watch stopped"
}
)
"watch stopped"
tuiLoop watchInputEnabled renderStateRef serverStatusRef TuiWatchStopped nextDashboard
Just TuiClean -> do
(_, nextDashboard) <-
runDashboardCommand
"clean"
"clean"
"manual clean"
"cleaning"
cleanCommandStatus
projectRoot
watchSettings
renderStateRef
serverStatusRef
dashboard
tuiLoop watchInputEnabled renderStateRef serverStatusRef TuiWatchStopped nextDashboard
Nothing -> case watchState of
TuiWatchStopped ->
tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
TuiWatching previousSnapshot -> do
nextSnapshot <- snapshotInputs projectRoot
if nextSnapshot == previousSnapshot
then tuiLoop watchInputEnabled renderStateRef serverStatusRef watchState dashboard
else do
let changedFiles = diffSnapshots previousSnapshot nextSnapshot
command :: String
command =
if any (`M.notMember` nextSnapshot) (M.keys previousSnapshot)
then "rebuild"
else "build"
changeSummary = intercalate ", " changedFiles
(_, nextDashboard) <-
runDashboardCommand
command
command
changeSummary
("building (" ++ command ++ ")")
(watchCommandStatus command)
projectRoot
watchSettings
renderStateRef
serverStatusRef
dashboard
tuiLoop watchInputEnabled renderStateRef serverStatusRef (TuiWatching nextSnapshot) nextDashboard
watchCommandStatus :: String -> ExitCode -> String
watchCommandStatus label exitCode
| exitCode == ExitSuccess = "watching"
| otherwise = "watching after failed " ++ label
cleanCommandStatus :: ExitCode -> String
cleanCommandStatus exitCode
| exitCode == ExitSuccess = "idle"
| otherwise = "idle after failed clean"
renderBuildResult :: ExitCode -> String
renderBuildResult ExitSuccess = "success"
renderBuildResult (ExitFailure code) = "failed (" ++ show code ++ ")"
runWatchBuild ::
appendDashboardMessage :: DashboardState -> String -> IO DashboardState
appendDashboardMessage dashboard message = do
timestamp <- watchTimestamp
pure (appendLogBatch dashboard message timestamp [])
runDashboardCommand ::
String ->
String ->
String ->
String ->
(ExitCode -> String) ->
FilePath ->
Configuration ->
WatchSettings ->
IORef (Maybe (TerminalSize, ServerStatus, DashboardState)) ->
IORef ServerStatus ->
DashboardState ->
IO (ExitCode, DashboardState)
runWatchBuild command label changeSummary projectRoot config watchSettings renderStateRef serverStatusRef dashboard = do
runDashboardCommand command label changeSummary runningStatus completedStatus projectRoot watchSettings renderStateRef serverStatusRef dashboard = do
startedAt <- watchTimestamp
let runningDashboard =
dashboard
{ dashboardStatus = "building (" ++ label ++ ")",
{ dashboardStatus = runningStatus,
dashboardLastChange = changeSummary,
dashboardLastBuild = "running since " ++ startedAt
}
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef runningDashboard
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef runningDashboard
(exitCode, buildLines) <- runCapturedSiteCommand projectRoot command
finishedAt <- watchTimestamp
let loggedDashboard =
appendLogBatch runningDashboard (label ++ ": " ++ changeSummary) finishedAt buildLines
completedDashboard =
loggedDashboard
{ dashboardStatus =
if exitCode == ExitSuccess
then "watching"
else "watching after failed " ++ label,
{ dashboardStatus = completedStatus exitCode,
dashboardLastBuild =
renderBuildResult exitCode
++ " at "
@@ -656,7 +746,7 @@ runWatchBuild command label changeSummary projectRoot config watchSettings rende
++ " via "
++ label
}
renderWatchDashboard renderStateRef projectRoot config watchSettings serverStatusRef completedDashboard
renderWatchDashboard renderStateRef projectRoot watchSettings serverStatusRef completedDashboard
pure (exitCode, completedDashboard)
startPreviewServer :: Configuration -> WatchSettings -> IORef ServerStatus -> IO ()