@ssebastien/tcmd 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -2
- package/bin/tcmd +17 -0
- package/package.json +37 -5
- package/src/js/tcmd/components/App.html +28 -0
- package/src/js/tcmd/components/App.js +355 -0
- package/src/js/tcmd/components/App.xml +98 -0
- package/src/js/tcmd/components/BookmarkList.html +12 -0
- package/src/js/tcmd/components/BookmarkList.js +73 -0
- package/src/js/tcmd/components/BookmarkList.xml +18 -0
- package/src/js/tcmd/components/BookmarkRow.html +5 -0
- package/src/js/tcmd/components/BookmarkRow.js +40 -0
- package/src/js/tcmd/components/BookmarkRow.xml +10 -0
- package/src/js/tcmd/components/Breadcrumbs.js +27 -0
- package/src/js/tcmd/components/CommandArgRow.js +90 -0
- package/src/js/tcmd/components/CommandArgRow.xml +5 -0
- package/src/js/tcmd/components/CommandBar.html +4 -0
- package/src/js/tcmd/components/CommandBar.js +126 -0
- package/src/js/tcmd/components/CommandBar.xml +4 -0
- package/src/js/tcmd/components/CommandItem.html +3 -0
- package/src/js/tcmd/components/CommandItem.js +78 -0
- package/src/js/tcmd/components/CommandItem.xml +6 -0
- package/src/js/tcmd/components/CommandPalette.html +7 -0
- package/src/js/tcmd/components/CommandPalette.js +58 -0
- package/src/js/tcmd/components/CommandPalette.xml +16 -0
- package/src/js/tcmd/components/CommandPanel.js +98 -0
- package/src/js/tcmd/components/CommandPanel.xml +35 -0
- package/src/js/tcmd/components/Confirm.html +3 -0
- package/src/js/tcmd/components/Confirm.js +36 -0
- package/src/js/tcmd/components/Confirm.xml +7 -0
- package/src/js/tcmd/components/Crumb.html +4 -0
- package/src/js/tcmd/components/Crumb.js +48 -0
- package/src/js/tcmd/components/Crumb.xml +4 -0
- package/src/js/tcmd/components/EntryDetail.html +15 -0
- package/src/js/tcmd/components/EntryDetail.js +194 -0
- package/src/js/tcmd/components/EntryDetail.xml +16 -0
- package/src/js/tcmd/components/FileList.html +1 -0
- package/src/js/tcmd/components/FileList.js +41 -0
- package/src/js/tcmd/components/FileList.xml +7 -0
- package/src/js/tcmd/components/FilePreview.html +5 -0
- package/src/js/tcmd/components/FilePreview.js +110 -0
- package/src/js/tcmd/components/FilePreview.xml +10 -0
- package/src/js/tcmd/components/FileRow.html +5 -0
- package/src/js/tcmd/components/FileRow.js +135 -0
- package/src/js/tcmd/components/FileRow.xml +10 -0
- package/src/js/tcmd/components/FilterBar.html +6 -0
- package/src/js/tcmd/components/FilterBar.js +50 -0
- package/src/js/tcmd/components/FilterBar.xml +6 -0
- package/src/js/tcmd/components/Help.xml +11 -0
- package/src/js/tcmd/components/PaletteRow.html +3 -0
- package/src/js/tcmd/components/PaletteRow.js +43 -0
- package/src/js/tcmd/components/PaletteRow.xml +4 -0
- package/src/js/tcmd/components/RowList.html +1 -0
- package/src/js/tcmd/components/RowList.xml +1 -0
- package/src/js/tcmd/components/Summary.html +3 -0
- package/src/js/tcmd/components/Summary.js +77 -0
- package/src/js/tcmd/components/Summary.xml +3 -0
- package/src/js/tcmd/components/bind.js +20 -0
- package/src/js/tcmd/components/format.js +155 -0
- package/src/js/tcmd/components/icons.js +185 -0
- package/src/js/tcmd/components/index.js +41 -0
- package/src/js/tcmd/components/rows.js +212 -0
- package/src/js/tcmd/core/bookmarks.js +175 -0
- package/src/js/tcmd/core/commands.js +930 -0
- package/src/js/tcmd/core/directory.js +194 -0
- package/src/js/tcmd/core/entries.js +88 -0
- package/src/js/tcmd/core/fs.js +266 -0
- package/src/js/tcmd/core/index.js +39 -0
- package/src/js/tcmd/core/keymap.js +60 -0
- package/src/js/tcmd/core/operations.js +453 -0
- package/src/js/tcmd/core/os.js +128 -0
- package/src/js/tcmd/core/pane.js +74 -0
- package/src/js/tcmd/core/panel.js +383 -0
- package/src/js/tcmd/core/path.js +57 -0
- package/src/js/tcmd/core/script.js +186 -0
- package/src/js/tcmd/core/selection.js +43 -0
- package/src/js/tcmd/core/session.js +405 -0
- package/src/js/tcmd/index.js +53 -0
- package/src/js/tcmd/tui/app.js +140 -0
- package/src/js/tcmd/tui/catalogue.js +300 -0
- package/src/js/tcmd/tui/frame.js +269 -0
- package/src/js/tcmd/tui/help.js +32 -0
- package/src/js/tcmd/tui/index.js +104 -0
- package/src/js/tcmd/tui/theme.js +177 -0
- package/src/js/tcmd/vendor/fzy.js +243 -0
- package/src/js/tcmd/web/app.js +154 -0
- package/src/js/tcmd/web/catalogue.js +159 -0
- package/src/js/tcmd/web/header.html +1 -0
- package/src/js/tcmd/web/header.js +38 -0
- package/src/js/tcmd/web/http.js +79 -0
- package/src/js/tcmd/web/index.js +22 -0
- package/src/js/tcmd/web/main.js +151 -0
- package/src/js/tcmd/web/opfs.js +282 -0
- package/src/js/tcmd/web/seed.js +90 -0
- package/src/js/tcmd/web/server.js +191 -0
- package/src/js/tcmd/web/theme.js +23 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!-- Root wrapper: the workspace, plus overlays positioned against the whole
|
|
2
|
+
viewport. The overlays stay INSIDE this root box so keypress routing (which
|
|
3
|
+
binds to it once) sees them as its focused subtree. -->
|
|
4
|
+
<box flexDirection="column" width="100%" height="100%" on:keypress="keys">
|
|
5
|
+
|
|
6
|
+
<!-- Workspace wrapper: always mounted, so the root box has one stable
|
|
7
|
+
child whether the workspace or the min-size gate shows. -->
|
|
8
|
+
<box flexGrow="1" flexDirection="column" overflow="hidden">
|
|
9
|
+
|
|
10
|
+
<!-- Identity bar: the one visible trace of TinyCmd itself (the host
|
|
11
|
+
aside, everything else on screen is the user's files). "Tiny"
|
|
12
|
+
shimmers blue letter by letter, "Cmd" shimmers orange. -->
|
|
13
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
14
|
+
<text out="brandT" out:fg="brandTFg" wrapMode="none"></text>
|
|
15
|
+
<text out="brandI" out:fg="brandIFg" wrapMode="none"></text>
|
|
16
|
+
<text out="brandN" out:fg="brandNFg" wrapMode="none"></text>
|
|
17
|
+
<text out="brandY" out:fg="brandYFg" wrapMode="none"></text>
|
|
18
|
+
<text out="brandC" out:fg="brandCFg" wrapMode="none"></text>
|
|
19
|
+
<text out="brandM" out:fg="brandMFg" wrapMode="none"></text>
|
|
20
|
+
<text out="brandD" out:fg="brandDFg" wrapMode="none"></text>
|
|
21
|
+
<text out="topBar" out:fg="topBarFg" wrapMode="none"></text>
|
|
22
|
+
</box>
|
|
23
|
+
|
|
24
|
+
<!-- Minimum-size gate: below the breakpoint the workspace is unusable. -->
|
|
25
|
+
<box when="tooSmall" flexGrow="1" alignItems="center" justifyContent="center" flexDirection="column" rowGap="1">
|
|
26
|
+
<text out="minSizeTitle" out:fg="titleFg"></text>
|
|
27
|
+
<text out="minSizeHint" out:fg="dimFg"></text>
|
|
28
|
+
</box>
|
|
29
|
+
|
|
30
|
+
<!-- Two boxed panes plus the inline inspector. -->
|
|
31
|
+
<box when="!tooSmall" flexDirection="column" flexGrow="1" overflow="hidden">
|
|
32
|
+
<box flexDirection="row" flexGrow="1" overflow="hidden">
|
|
33
|
+
<box flexDirection="column" flexGrow="1" border borderStyle="single"
|
|
34
|
+
out:border="leftBorder" out:borderColor="leftBorderFg"
|
|
35
|
+
on:click="leftId!pane:activate">
|
|
36
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
37
|
+
<box when="!leftFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
38
|
+
<breadcrumbs in:segments="leftSegments" in:paneId="leftId" height="1"></breadcrumbs>
|
|
39
|
+
</box>
|
|
40
|
+
<filterbar in:query="leftFilter" in:paneId="leftId" in:filtering="filtering" in:entries="leftEntries"></filterbar>
|
|
41
|
+
</box>
|
|
42
|
+
<box height="1" out:border="headRule" out:borderColor="leftHeadRuleFg"></box>
|
|
43
|
+
<filelist in:entries="leftEntries" in:cursor="leftCursor" in:marked="leftMarked"
|
|
44
|
+
in:active="leftActive" in:paneId="leftId" in:loading="leftLoading"
|
|
45
|
+
in:viewport="listViewport"></filelist>
|
|
46
|
+
<entrydetail in:entry="leftEntry" in:marked="leftMarked" height="2"></entrydetail>
|
|
47
|
+
<summary in:entries="leftEntries" in:disk="leftDisk" height="1"></summary>
|
|
48
|
+
</box>
|
|
49
|
+
<box flexDirection="column" flexGrow="1" border borderStyle="single"
|
|
50
|
+
out:border="rightBorder" out:borderColor="rightBorderFg"
|
|
51
|
+
on:click="rightId!pane:activate">
|
|
52
|
+
<box flexDirection="row" height="1" paddingLeft="1" overflow="hidden">
|
|
53
|
+
<box when="!rightFilterOn" flexGrow="1" flexShrink="1" minWidth="0" overflow="hidden">
|
|
54
|
+
<breadcrumbs in:segments="rightSegments" in:paneId="rightId" height="1"></breadcrumbs>
|
|
55
|
+
</box>
|
|
56
|
+
<filterbar in:query="rightFilter" in:paneId="rightId" in:filtering="filtering" in:entries="rightEntries"></filterbar>
|
|
57
|
+
</box>
|
|
58
|
+
<box height="1" out:border="headRule" out:borderColor="rightHeadRuleFg"></box>
|
|
59
|
+
<filelist in:entries="rightEntries" in:cursor="rightCursor" in:marked="rightMarked"
|
|
60
|
+
in:active="rightActive" in:paneId="rightId" in:loading="rightLoading"
|
|
61
|
+
in:viewport="listViewport"></filelist>
|
|
62
|
+
<entrydetail in:entry="rightEntry" in:marked="rightMarked" height="2"></entrydetail>
|
|
63
|
+
<summary in:entries="rightEntries" in:disk="rightDisk" height="1"></summary>
|
|
64
|
+
</box>
|
|
65
|
+
<filepreview when="showInspectorInline" in:preview="preview" in:show="showInspectorInline"
|
|
66
|
+
in:entry="appEntry" flexGrow="1"></filepreview>
|
|
67
|
+
</box>
|
|
68
|
+
<commandbar in:items="commandBarItems" in:notice="notice" in:leftError="leftError"
|
|
69
|
+
in:rightError="rightError" in:active="active"
|
|
70
|
+
in:leftMarked="leftMarked" in:rightMarked="rightMarked" out:height="barHeight"></commandbar>
|
|
71
|
+
</box>
|
|
72
|
+
</box>
|
|
73
|
+
|
|
74
|
+
<!-- Fullscreen inspector for narrow viewports. -->
|
|
75
|
+
<box when="showViewer" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="10"
|
|
76
|
+
alignItems="center" justifyContent="center">
|
|
77
|
+
<box width="80%" height="90%">
|
|
78
|
+
<filepreview in:preview="preview" in:show="showViewer" in:entry="appEntry"></filepreview>
|
|
79
|
+
</box>
|
|
80
|
+
</box>
|
|
81
|
+
|
|
82
|
+
<commandpalette when="paletteOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="20"
|
|
83
|
+
in:open="paletteOpen" in:mode="paletteMode" in:query="paletteQuery"
|
|
84
|
+
in:items="paletteItems" in:selected="paletteIndex"></commandpalette>
|
|
85
|
+
<commandpanel when="panelOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="22"
|
|
86
|
+
in:open="panelOpen" in:operation="panelOperation" in:items="panelItems"
|
|
87
|
+
in:opCursor="panelOpIndex" in:rows="panelRows" in:argCursor="panelRowIndex"
|
|
88
|
+
in:focus="panelFocus" in:textual="panelTextual" in:argRow="panelCursorRow"
|
|
89
|
+
in:command="panelCommand" in:runnable="panelRunnable"
|
|
90
|
+
in:query="panelQuery" in:value="panelValue"></commandpanel>
|
|
91
|
+
<bookmarklist when="bookmarkOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="25"
|
|
92
|
+
in:open="bookmarkOpen" in:mode="bookmarkMode" in:query="bookmarkQuery"
|
|
93
|
+
in:target="bookmarkTarget" in:items="bookmarkItems" in:selected="bookmarkIndex"></bookmarklist>
|
|
94
|
+
<confirm when="confirm" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="30"
|
|
95
|
+
in:confirm="confirm"></confirm>
|
|
96
|
+
<help when="helpOpen" position="absolute" top="0" left="0" width="100%" height="100%" zIndex="40"
|
|
97
|
+
in:open="helpOpen"></help>
|
|
98
|
+
</box>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<div class="bookmark-list" when="open">
|
|
2
|
+
<div class="bookmark-dialog">
|
|
3
|
+
<header>
|
|
4
|
+
<span class="bookmark-sign" out="sign"></span>
|
|
5
|
+
<input class="bookmark-query" type="text" inout="query" on:enter="confirm">
|
|
6
|
+
</header>
|
|
7
|
+
<p class="bookmark-target" when="inputVisible" out="target"></p>
|
|
8
|
+
<p class="bookmark-empty" when="emptyList" out="empty"></p>
|
|
9
|
+
<ul class="bookmark-rows" when="listVisible" out="rows"></ul>
|
|
10
|
+
<footer out="footer"></footer>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-09
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/BookmarkList
|
|
7
|
+
// BookmarkList: the `g`/`b` overlay. In "name" mode it shows an input to name
|
|
8
|
+
// the path being recorded; in "list" mode it shows the fuzzy picker over the
|
|
9
|
+
// stored bookmarks (matched by name or path). The row logic lives in
|
|
10
|
+
// `BookmarkRow.js`; this parent maps its `items` to rows.
|
|
11
|
+
//
|
|
12
|
+
// Environment: `overlayBg`, `borderPrimaryActiveFg`, `textSecondaryFg`
|
|
13
|
+
// (footer/path), `textHeadingFg` (title), `rows(data)` returning the keyed rows
|
|
14
|
+
// and `scrollTop(self, data)` (TUI viewport tracking).
|
|
15
|
+
|
|
16
|
+
import { unwrap } from "select/state/cells"
|
|
17
|
+
|
|
18
|
+
// Function: title
|
|
19
|
+
// The overlay title: "bookmark" while naming, "bookmarks" while picking.
|
|
20
|
+
function bookmarkTitle(mode) {
|
|
21
|
+
return unwrap(mode) === "name" ? "bookmark" : "bookmarks"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Function: footer
|
|
25
|
+
function bookmarkFooter(mode) {
|
|
26
|
+
return unwrap(mode) === "name"
|
|
27
|
+
? "name the bookmark ↵ save Esc cancel"
|
|
28
|
+
: "↑↓ move ↵ jump Esc close"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Function: targetLine
|
|
32
|
+
// The path being recorded (name mode). `target` is `{ path, isDir }` or null.
|
|
33
|
+
function targetLine(target) {
|
|
34
|
+
const value = unwrap(target)
|
|
35
|
+
if (!value) return ""
|
|
36
|
+
if (typeof value === "string") return value
|
|
37
|
+
return `${value.path ?? ""}${value.isDir === true ? "/" : ""}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Function: createBehaviors
|
|
41
|
+
// Builds the BookmarkList behavior map for the given renderer environment.
|
|
42
|
+
function createBehaviors(env) {
|
|
43
|
+
const behaviors = {
|
|
44
|
+
bg: () => env.overlayBg,
|
|
45
|
+
borderFg: () => env.borderPrimaryActiveFg,
|
|
46
|
+
confirm: (self, data) => self.pub("bookmark:confirm"),
|
|
47
|
+
footer: (_self, data) => bookmarkFooter(data.mode),
|
|
48
|
+
footerFg: () => env.textSecondaryFg,
|
|
49
|
+
// The input is focused/typing in name mode; the list navigates otherwise.
|
|
50
|
+
focused: (_self, data) => Boolean(unwrap(data.open)),
|
|
51
|
+
inputVisible: (_self, data) => unwrap(data.mode) === "name",
|
|
52
|
+
listVisible: (_self, data) => unwrap(data.mode) === "list",
|
|
53
|
+
open: (_self, data) => Boolean(unwrap(data.open)),
|
|
54
|
+
pathFg: () => env.textSecondaryFg,
|
|
55
|
+
sign: (_self, data) => (unwrap(data.mode) === "name" ? "+" : "›"),
|
|
56
|
+
target: (_self, data) => targetLine(data.target),
|
|
57
|
+
title: (_self, data) => bookmarkTitle(data.mode),
|
|
58
|
+
titleColor: () => env.textHeadingFg,
|
|
59
|
+
emptyList: (_self, data) =>
|
|
60
|
+
unwrap(data.mode) === "list" && (unwrap(data.items)?.length ?? 0) === 0,
|
|
61
|
+
empty: () => " no bookmarks",
|
|
62
|
+
emptyFg: () => env.textSecondaryFg,
|
|
63
|
+
}
|
|
64
|
+
if (env?.rows) {
|
|
65
|
+
behaviors.rows = (_self, data) => env.rows(self, data)
|
|
66
|
+
}
|
|
67
|
+
if (env?.scrollTop) {
|
|
68
|
+
behaviors.scrollTop = (self, data) => env.scrollTop(self, data)
|
|
69
|
+
}
|
|
70
|
+
return behaviors
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { bookmarkTitle, createBehaviors }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<box when="open" width="100%" height="100%" alignItems="center" justifyContent="center">
|
|
2
|
+
<box width="72%" height="60%" border borderStyle="rounded"
|
|
3
|
+
out:title="title" out:titleColor="titleColor" out:borderColor="borderFg"
|
|
4
|
+
out:backgroundColor="bg" flexDirection="column">
|
|
5
|
+
<box flexDirection="row" height="1" paddingLeft="1">
|
|
6
|
+
<text out="sign" out:fg="titleColor"></text>
|
|
7
|
+
<input ref="query" inout="query" flexGrow="1" height="1" out:focused="focused" on:enter="confirm"></input>
|
|
8
|
+
</box>
|
|
9
|
+
<text when="inputVisible" out="target" out:fg="pathFg" paddingLeft="1" wrapMode="none" truncate="true"></text>
|
|
10
|
+
<text when="emptyList" out="empty" out:fg="emptyFg" paddingLeft="1" wrapMode="none"></text>
|
|
11
|
+
<scrollbox when="listVisible" ref="list" flexGrow="1">
|
|
12
|
+
<box out="rows" flexDirection="column"></box>
|
|
13
|
+
</scrollbox>
|
|
14
|
+
<box flexDirection="row" height="1" paddingLeft="1">
|
|
15
|
+
<text out="footer" out:fg="footerFg"></text>
|
|
16
|
+
</box>
|
|
17
|
+
</box>
|
|
18
|
+
</box>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-09
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/BookmarkRow
|
|
7
|
+
// One row of the bookmark picker. Reads `{ item: { name, path }, index,
|
|
8
|
+
// selected }`, the same bag shape in both engines. Selection is snapshotted by
|
|
9
|
+
// the row builder into a plain `selectedRow` boolean and read through the
|
|
10
|
+
// shared `isSelected` predicate, with the `selected` cell as fallback for
|
|
11
|
+
// hand-built bags.
|
|
12
|
+
//
|
|
13
|
+
// Environment: `accentFg`, `textFg`, `textSecondaryFg`, `selectionBg` (TUI row
|
|
14
|
+
// background), `selectedClass` (web), `icons` (show Nerd Font icons).
|
|
15
|
+
|
|
16
|
+
import { isSelected } from "./rows.js"
|
|
17
|
+
import { iconFor } from "./icons.js"
|
|
18
|
+
|
|
19
|
+
// Function: createBehaviors
|
|
20
|
+
// Builds the BookmarkRow behavior map for the given renderer environment.
|
|
21
|
+
function createBehaviors(env) {
|
|
22
|
+
return {
|
|
23
|
+
rowBg: (_self, data) => (isSelected(data) ? env.selectionBg : undefined),
|
|
24
|
+
rowClass: (_self, data) => (isSelected(data) ? env.selectedClass : null),
|
|
25
|
+
rowFg: (_self, data) => (isSelected(data) ? env.accentFg : env.textFg),
|
|
26
|
+
// The icon is its own slot, dimmer than the name; it collapses when
|
|
27
|
+
// the environment keeps rows icon-free.
|
|
28
|
+
hasIcon: () => Boolean(env.icons),
|
|
29
|
+
rowIcon: (_self, data) => (env.icons ? iconFor(data.item) : ""),
|
|
30
|
+
rowIconFg: () => env.textSecondaryFg,
|
|
31
|
+
rowMarker: (_self, data) => (isSelected(data) ? ">" : " "),
|
|
32
|
+
// A directory bookmark reads as `name/`, mirroring the listing.
|
|
33
|
+
rowName: (_self, data) =>
|
|
34
|
+
`${data.item?.name ?? ""}${data.item?.isDir === true ? "/" : ""}`,
|
|
35
|
+
rowPath: (_self, data) => `${data.item?.path ?? ""}`,
|
|
36
|
+
rowPathFg: () => env.textSecondaryFg,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { createBehaviors }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="item.name!bookmark:select">
|
|
2
|
+
<text out="rowMarker" width="2" out:fg="rowFg"></text>
|
|
3
|
+
<text out="rowIcon" out:fg="rowIconFg" wrapMode="none" when="hasIcon"></text>
|
|
4
|
+
<box minWidth="12" flexShrink="0">
|
|
5
|
+
<text out="rowName" out:fg="rowFg" wrapMode="none" truncate="true"></text>
|
|
6
|
+
</box>
|
|
7
|
+
<box flexGrow="1" minWidth="0" overflow="hidden">
|
|
8
|
+
<text out="rowPath" out:fg="rowPathFg" wrapMode="none" truncate="true"></text>
|
|
9
|
+
</box>
|
|
10
|
+
</box>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-26
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/Breadcrumbs
|
|
7
|
+
// PathBreadcrumbs: clickable path elements for one pane. The row logic lives
|
|
8
|
+
// in `Crumb.js`; this parent only maps its `segments` to `Crumb` rows and
|
|
9
|
+
// exposes the web root class.
|
|
10
|
+
//
|
|
11
|
+
// Environment: `rows(data)` returning the keyed row list and `listClass`
|
|
12
|
+
// (web root class); both are injected by the renderer binding. The TUI binds
|
|
13
|
+
// this component to the shared `RowList.xml`, the web to `RowList.html`.
|
|
14
|
+
|
|
15
|
+
// Function: createBehaviors
|
|
16
|
+
// Builds the Breadcrumbs behavior map for the given renderer environment.
|
|
17
|
+
function createBehaviors(env) {
|
|
18
|
+
const behaviors = { listClass: () => env?.listClass }
|
|
19
|
+
if (env?.rows) {
|
|
20
|
+
behaviors.rows = (_self, data) => env.rows(self, data)
|
|
21
|
+
}
|
|
22
|
+
return behaviors
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { createBehaviors }
|
|
26
|
+
|
|
27
|
+
// EOF
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-10-09
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/CommandArgRow
|
|
7
|
+
// CommandArgRow: one row shared by both command-panel columns. The operation
|
|
8
|
+
// column passes `{ type: "op" }` rows (name = operation title, summary =
|
|
9
|
+
// signature, `risk` tints destructive/caution operations); the argument column
|
|
10
|
+
// passes `{ type: "arg" | "item" }` rows from `core/panel.panelRowList`. An
|
|
11
|
+
// item shows a `[x]`/`[ ]` toggle with its basename and, dimmed after it, the
|
|
12
|
+
// parent directory; a focused row carries a `>` marker so focus is not
|
|
13
|
+
// colour-only. Reads the row bag `{ item, index, row, cursor, focus }` built by
|
|
14
|
+
// `rows.js`.
|
|
15
|
+
//
|
|
16
|
+
// Environment: `accentFg` (focused row), `textFg`, `textSecondaryFg` (summary),
|
|
17
|
+
// `accentSecondaryFg` (a selected item), `errorFg` (destructive op),
|
|
18
|
+
// `warningFg` (caution op), `selectionBg` (row background), `focusedClass`
|
|
19
|
+
// (web).
|
|
20
|
+
|
|
21
|
+
import { unwrap } from "select/state/cells"
|
|
22
|
+
|
|
23
|
+
// Function: riskFg
|
|
24
|
+
// The colour a row's risk maps to: red for destructive operations, amber for
|
|
25
|
+
// caution (a move), null when the row is not risky.
|
|
26
|
+
function riskFg(row, env) {
|
|
27
|
+
if (row?.type === "op" && row.risk === "destructive") return env.errorFg
|
|
28
|
+
if (row?.type === "op" && row.risk === "caution") return env.warningFg
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Function: rowName
|
|
33
|
+
// The row's left cell: the operation title, an argument's name, or an item's
|
|
34
|
+
// `[x] basename`.
|
|
35
|
+
function rowName(row) {
|
|
36
|
+
if (!row) return ""
|
|
37
|
+
if (row.type === "item") return `${row.marker ?? "[ ]"} ${row.name ?? ""}`
|
|
38
|
+
return row.name ?? ""
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Function: rowSummary
|
|
42
|
+
// The row's right cell: the operation signature, an argument's binding
|
|
43
|
+
// summary, or an item's parent directory.
|
|
44
|
+
function rowSummary(row) {
|
|
45
|
+
if (!row) return ""
|
|
46
|
+
if (row.type === "item") return row.parent ?? ""
|
|
47
|
+
if (row.type === "op") return row.summary ?? ""
|
|
48
|
+
return row.summary ?? ""
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Function: wantsFocus
|
|
52
|
+
// The focus token a row belongs to: the operation column ("ops") or the
|
|
53
|
+
// argument column ("args").
|
|
54
|
+
function wantsFocus(row) {
|
|
55
|
+
return row?.type === "op" ? "ops" : "args"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Function: isFocused
|
|
59
|
+
// True when this row owns the cursor in the focused column.
|
|
60
|
+
function isFocused(row, index, cursor, focus) {
|
|
61
|
+
if (!row || !focus) return false
|
|
62
|
+
return focus === wantsFocus(row) && index === cursor
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Function: createBehaviors
|
|
66
|
+
// Builds the CommandArgRow behavior map for the given renderer environment.
|
|
67
|
+
function createBehaviors(env) {
|
|
68
|
+
const focused = (data) => isFocused(data.row, data.index, unwrap(data.cursor), unwrap(data.focus))
|
|
69
|
+
return {
|
|
70
|
+
marker: (_self, data) => (focused(data) ? ">" : " "),
|
|
71
|
+
markerFg: () => env.accentFg,
|
|
72
|
+
name: (_self, data) => rowName(data.row),
|
|
73
|
+
rowBg: (_self, data) => (focused(data) ? env.selectionBg : undefined),
|
|
74
|
+
rowClass: (_self, data) => (focused(data) ? env.focusedClass : null),
|
|
75
|
+
rowFg: (_self, data) => {
|
|
76
|
+
// A risky operation (delete/erase/move) is always tinted — even
|
|
77
|
+
// focused — so danger reads at a glance; focus is carried by the
|
|
78
|
+
// `>` marker and the row background instead.
|
|
79
|
+
const risk = riskFg(data.row, env)
|
|
80
|
+
if (risk) return risk
|
|
81
|
+
if (focused(data)) return env.accentFg
|
|
82
|
+
if (data.row?.type === "item" && data.row?.selected) return env.accentSecondaryFg
|
|
83
|
+
return env.textFg
|
|
84
|
+
},
|
|
85
|
+
summary: (_self, data) => rowSummary(data.row),
|
|
86
|
+
summaryFg: () => env.textSecondaryFg,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { createBehaviors, isFocused, riskFg, rowName, rowSummary, wantsFocus }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" columnGap="1" out:backgroundColor="rowBg" on:click="row.key!panel:select">
|
|
2
|
+
<text out="marker" width="2" out:fg="markerFg"></text>
|
|
3
|
+
<text out="name" flexShrink="0" out:fg="rowFg" wrapMode="none"></text>
|
|
4
|
+
<text out="summary" flexGrow="1" flexShrink="1" minWidth="0" out:fg="summaryFg" wrapMode="none" truncate="true"></text>
|
|
5
|
+
</box>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-27
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/CommandBar
|
|
7
|
+
// CommandBar: the bottom bar. By default it shows the keys available in the
|
|
8
|
+
// current context (the command registry's `bar` commands, or the palette
|
|
9
|
+
// hints while the palette is open); a pane error or a session notice takes
|
|
10
|
+
// its place. Entries are bracket shorthands (see `CommandItem.js`) that wrap
|
|
11
|
+
// onto as many rows as the width demands; the active pane's marked count
|
|
12
|
+
// joins the list as a dimmed, non-clickable entry. The row logic lives in
|
|
13
|
+
// `CommandItem.js`; this parent maps its `items` to rows.
|
|
14
|
+
//
|
|
15
|
+
// The web presentation swaps the key list for a single status line
|
|
16
|
+
// (`showKeys`/`showStatus`/`status`/`statusClass`); the TUI presentation
|
|
17
|
+
// keeps the wrapped key list and swaps it for the status when there is one.
|
|
18
|
+
//
|
|
19
|
+
// Environment: `rows(data)` returning the keyed row list, `listClass` (web
|
|
20
|
+
// root class), `errorClass` (web), and the TUI colors plus the
|
|
21
|
+
// error/warning/tip foreground/background pairs for the status badge.
|
|
22
|
+
|
|
23
|
+
import { unwrap } from "select/state/cells"
|
|
24
|
+
import { commandWidth } from "./CommandItem.js"
|
|
25
|
+
|
|
26
|
+
// Function: commandBarStatus
|
|
27
|
+
// The one-line status the bar shows when something needs attention: the
|
|
28
|
+
// session notice first, then a pane error. Empty when everything is fine.
|
|
29
|
+
function commandBarStatus(data) {
|
|
30
|
+
const notice = unwrap(data?.notice)
|
|
31
|
+
if (notice) {
|
|
32
|
+
const text = notice.text ?? `${notice}`
|
|
33
|
+
return notice.kind === "error" ? `error: ${text}` : text
|
|
34
|
+
}
|
|
35
|
+
const error = unwrap(data?.leftError) ?? unwrap(data?.rightError)
|
|
36
|
+
return error ? `error: ${error}` : ""
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Function: commandBarMarks
|
|
40
|
+
// Count of marked entries in the active pane, for the status bar's right zone.
|
|
41
|
+
function commandBarMarks(data) {
|
|
42
|
+
const active = unwrap(data?.active)
|
|
43
|
+
const marked = active === "left" ? unwrap(data?.leftMarked) : unwrap(data?.rightMarked)
|
|
44
|
+
const count = marked?.size ?? 0
|
|
45
|
+
return count > 0 ? `${count} marked` : ""
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Function: commandBarMarksItem
|
|
49
|
+
// The marked count as a trailing, non-clickable bar entry (null when none).
|
|
50
|
+
function commandBarMarksItem(data) {
|
|
51
|
+
const marks = commandBarMarks(data)
|
|
52
|
+
return marks ? { id: null, title: marks, dim: true } : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Function: commandBarRowCount
|
|
56
|
+
// How many rows the wrapped entries occupy at `width` columns (greedy fill,
|
|
57
|
+
// matching the flex layout, separators included). `width <= 0` (unknown)
|
|
58
|
+
// assumes a single row.
|
|
59
|
+
function commandBarRowCount(items, width) {
|
|
60
|
+
if (!width || width <= 0) return 1
|
|
61
|
+
let rows = 1
|
|
62
|
+
let cols = 0
|
|
63
|
+
let index = 0
|
|
64
|
+
for (const item of items ?? []) {
|
|
65
|
+
const w = commandWidth(item, index)
|
|
66
|
+
if (cols > 0 && cols + w > width) {
|
|
67
|
+
rows++
|
|
68
|
+
cols = 0
|
|
69
|
+
}
|
|
70
|
+
cols += w
|
|
71
|
+
index++
|
|
72
|
+
}
|
|
73
|
+
return rows
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Function: isInfo
|
|
77
|
+
// True when the bar status comes from a non-error notice.
|
|
78
|
+
function isInfo(data) {
|
|
79
|
+
const notice = unwrap(data?.notice)
|
|
80
|
+
return Boolean(notice && notice.kind !== "error")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Function: statusKind
|
|
84
|
+
// The notice kind driving the status (null when there is no notice).
|
|
85
|
+
function statusKind(data) {
|
|
86
|
+
return unwrap(data?.notice)?.kind ?? null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Function: createBehaviors
|
|
90
|
+
// Builds the CommandBar behavior map for the given renderer environment.
|
|
91
|
+
function createBehaviors(env) {
|
|
92
|
+
const behaviors = {
|
|
93
|
+
listClass: () => env?.listClass,
|
|
94
|
+
showKeys: (_self, data) => !commandBarStatus(data),
|
|
95
|
+
showStatus: (_self, data) => Boolean(commandBarStatus(data)),
|
|
96
|
+
status: (_self, data) => commandBarStatus(data),
|
|
97
|
+
statusBg: (_self, data) => {
|
|
98
|
+
const kind = statusKind(data)
|
|
99
|
+
if (kind === "warning") return env?.warningBg
|
|
100
|
+
if (kind === "tip") return env?.tipBg
|
|
101
|
+
if (kind === "error" || unwrap(data?.leftError) || unwrap(data?.rightError)) return env?.errorBg
|
|
102
|
+
return undefined
|
|
103
|
+
},
|
|
104
|
+
statusClass: (_self, data) => (commandBarStatus(data) && !isInfo(data) ? env?.errorClass : null),
|
|
105
|
+
statusFg: (_self, data) => {
|
|
106
|
+
const kind = statusKind(data)
|
|
107
|
+
if (kind === "warning") return env?.warningFg
|
|
108
|
+
if (kind === "tip") return env?.tipFg
|
|
109
|
+
if (kind === "error" || unwrap(data?.leftError) || unwrap(data?.rightError)) return env?.errorFg
|
|
110
|
+
return env?.accentFg
|
|
111
|
+
},
|
|
112
|
+
// TUI: a padded status for the right zone, empty when all is well.
|
|
113
|
+
statusPad: (_self, data) => {
|
|
114
|
+
const text = commandBarStatus(data)
|
|
115
|
+
return text ? ` ${text} ` : ""
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
if (env?.rows) {
|
|
119
|
+
behaviors.rows = (_self, data) => env.rows(self, data)
|
|
120
|
+
}
|
|
121
|
+
return behaviors
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { commandBarMarks, commandBarMarksItem, commandBarRowCount, commandBarStatus, createBehaviors }
|
|
125
|
+
|
|
126
|
+
// EOF
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Project: TinyCommand
|
|
2
|
+
// Author: Sebastien Pierre
|
|
3
|
+
// License: BSD
|
|
4
|
+
// Created: 2026-09-27
|
|
5
|
+
|
|
6
|
+
// Module: tcmd/components/CommandItem
|
|
7
|
+
// Command-bar entry behaviors. Read `{ item, index }`, the same bag shape in
|
|
8
|
+
// both engines. `item.id` may be null for pure key hints, in which case a
|
|
9
|
+
// click dispatches nothing.
|
|
10
|
+
//
|
|
11
|
+
// Each entry renders as a plain bracket shorthand, preceded by a dim `|`
|
|
12
|
+
// divider (except the first entry):
|
|
13
|
+
//
|
|
14
|
+
// [Open ⏎] — the bracket and title in the text colour, the key glyph in
|
|
15
|
+
// the accent (primary) or secondary accent, and a trailing
|
|
16
|
+
// space separating it from the next entry.
|
|
17
|
+
//
|
|
18
|
+
// Entries without a key (the marked count) render as a dimmed plain word
|
|
19
|
+
// with no brackets. The web renders the same label as a single string (its
|
|
20
|
+
// buttons are separated by their gap, not the pipe).
|
|
21
|
+
//
|
|
22
|
+
// Environment: `accentFg` (primary), `accentSecondaryFg` (secondary),
|
|
23
|
+
// `textFg`, `textSecondaryFg`.
|
|
24
|
+
|
|
25
|
+
// Function: keyGlyph
|
|
26
|
+
// The shorthand glyph for a shortcut label: ⏎ for Enter, ⇥ for Tab.
|
|
27
|
+
function keyGlyph(keys) {
|
|
28
|
+
const GLYPHS = { Enter: "⏎", "↵": "⏎", Tab: "⇥" }
|
|
29
|
+
return GLYPHS[keys] ?? keys
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Function: commandLabel
|
|
33
|
+
// "[Open ⏎] " style label; entries without a key are just a plain word.
|
|
34
|
+
function commandLabel(item) {
|
|
35
|
+
if (!item) return ""
|
|
36
|
+
return item.keys ? `[${item.title} ${keyGlyph(item.keys)}] ` : `${item.title} `
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Constant: COMMAND_SEP
|
|
40
|
+
// The divider drawn before every entry but the first: a dim pipe and the
|
|
41
|
+
// space that keeps it clear of the button.
|
|
42
|
+
const COMMAND_SEP = "| "
|
|
43
|
+
|
|
44
|
+
// Function: commandWidth
|
|
45
|
+
// Columns the entry occupies: its label plus the leading separator when it
|
|
46
|
+
// is not the first entry.
|
|
47
|
+
function commandWidth(item, index) {
|
|
48
|
+
return commandLabel(item).length + (index > 0 ? COMMAND_SEP.length : 0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Function: isPrimary
|
|
52
|
+
// True when the item is its context's most important action.
|
|
53
|
+
function isPrimary(item) {
|
|
54
|
+
return Boolean(item?.primary)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Function: createBehaviors
|
|
58
|
+
// Builds the CommandItem behavior map for the given renderer environment.
|
|
59
|
+
function createBehaviors(env) {
|
|
60
|
+
const head = (_self, data) => (data.item?.keys ? `[${data.item.title} ` : `${data.item?.title ?? ""} `)
|
|
61
|
+
return {
|
|
62
|
+
// Web: the whole shorthand as one label.
|
|
63
|
+
itemLabel: (_self, data) => commandLabel(data.item),
|
|
64
|
+
// TUI: the dim pipe divider, then the bracket + title head, the
|
|
65
|
+
// accent key glyph, the closer.
|
|
66
|
+
itemSep: (_self, data) => (data.index > 0 ? COMMAND_SEP : ""),
|
|
67
|
+
itemSepFg: () => env?.textSecondaryFg,
|
|
68
|
+
itemHead: head,
|
|
69
|
+
itemFg: (_self, data) => (data.item?.keys ? env?.textFg : env?.textSecondaryFg),
|
|
70
|
+
keyText: (_self, data) => (data.item?.keys ? keyGlyph(data.item.keys) : ""),
|
|
71
|
+
keyFg: (_self, data) => (isPrimary(data.item) ? env?.accentFg : env?.accentSecondaryFg),
|
|
72
|
+
endText: (_self, data) => (data.item?.keys ? "] " : ""),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { COMMAND_SEP, commandLabel, commandWidth, createBehaviors, isPrimary, keyGlyph }
|
|
77
|
+
|
|
78
|
+
// EOF
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<box flexDirection="row" height="1" flexShrink="0" on:click="item.id!commandbar:run">
|
|
2
|
+
<text out="itemSep" out:fg="itemSepFg" wrapMode="none"></text>
|
|
3
|
+
<text out="itemHead" out:fg="itemFg" wrapMode="none"></text>
|
|
4
|
+
<text out="keyText" out:fg="keyFg" wrapMode="none"></text>
|
|
5
|
+
<text out="endText" out:fg="itemFg" wrapMode="none"></text>
|
|
6
|
+
</box>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<section class="palette" when="open">
|
|
2
|
+
<div class="palette-line">
|
|
3
|
+
<span class="palette-sign" out="sign"></span>
|
|
4
|
+
<input ref="query" inout="query" class="palette-input" placeholder="Command..." on:enter="confirm" />
|
|
5
|
+
</div>
|
|
6
|
+
<ul class="palette-list" out="rows"></ul>
|
|
7
|
+
</section>
|