Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
!*.txt
!*.wasm
!*.qml
!*.js
!*.ts
!qmldir
!*.mjs
!*.sh

# Specific project files
!.gitignore
Expand All @@ -35,3 +40,4 @@
memory/
.claude/
.vscode/
node_modules/
104 changes: 104 additions & 0 deletions cmd/tilbo-daemon/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,107 @@ func applyTagsXattr(ctx context.Context, path string, tagNames []string, op stri
}
return store.WriteTags(ctx, path, merged)
}

func handleListDirectory(req *ipcv1.ListDirectoryRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
entries, err := browser.ListDirectory(req.GetPath(), req.GetHidden())
if err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
resEntries := make([]*ipcv1.DirEntry, len(entries))
for i, e := range entries {
resEntries[i] = &ipcv1.DirEntry{
Name: e.Name,
Path: e.Path,
IsDir: e.IsDir,
SizeBytes: e.Size,
Mtime: e.MTime,
Mode: e.Mode,
Hidden: e.Hidden,
}
}
return &ipcv1.Response{Kind: &ipcv1.Response_ListDirectory{
ListDirectory: &ipcv1.ListDirectoryResponse{Entries: resEntries},
}}, nil
}

func handleStatFile(req *ipcv1.StatFileRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
stat, err := browser.StatFile(req.GetPath())
if err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
return &ipcv1.Response{Kind: &ipcv1.Response_StatFile{
StatFile: &ipcv1.StatFileResponse{
Stat: &ipcv1.FileStat{
SizeBytes: stat.Size,
Mtime: stat.MTime,
Mode: stat.Mode,
},
},
}}, nil
}

func handleGlobSearch(req *ipcv1.GlobSearchRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
files, err := browser.GlobSearch(req.GetPatterns(), req.GetLimit(), req.GetAllowHidden())
if err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
resFiles := make([]*ipcv1.FileResult, len(files))
for i, f := range files {
resFiles[i] = &ipcv1.FileResult{
Path: f.Path,
Tags: f.Tags,
Metadata: f.Metadata,
Score: f.Score,
Mtime: f.MTime,
SizeBytes: f.Size,
}
}
return &ipcv1.Response{Kind: &ipcv1.Response_GlobSearch{
GlobSearch: &ipcv1.GlobSearchResponse{Files: resFiles},
}}, nil
}

func handleRenameFile(req *ipcv1.RenameFileRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
newPath, err := browser.RenameFile(req.GetPath(), req.GetNewName())
if err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
return &ipcv1.Response{Kind: &ipcv1.Response_RenameFile{
RenameFile: &ipcv1.RenameFileResponse{NewPath: newPath},
}}, nil
}

func handleDeleteFile(req *ipcv1.DeleteFileRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
if err := browser.DeleteFile(req.GetPath()); err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
return &ipcv1.Response{Kind: &ipcv1.Response_DeleteFile{
DeleteFile: &ipcv1.DeleteFileResponse{},
}}, nil
}

func handleChmodFile(req *ipcv1.ChmodFileRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
if err := browser.ChmodFile(req.GetPath(), req.GetMode()); err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
return &ipcv1.Response{Kind: &ipcv1.Response_ChmodFile{
ChmodFile: &ipcv1.ChmodFileResponse{},
}}, nil
}

func handleListPlaces(_ *ipcv1.ListPlacesRequest, browser *daemonBrowserMethods) (*ipcv1.Response, error) {
places, err := browser.ListPlaces()
if err != nil {
return errResponse(daemonInternalErrCode, err.Error()), nil
}
resPlaces := make([]*ipcv1.PlaceEntry, len(places))
for i, p := range places {
resPlaces[i] = &ipcv1.PlaceEntry{
Name: p.Name,
Path: p.Path,
}
}
return &ipcv1.Response{Kind: &ipcv1.Response_ListPlaces{
ListPlaces: &ipcv1.ListPlacesResponse{Places: resPlaces},
}}, nil
}
97 changes: 46 additions & 51 deletions cmd/tilbo-daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
ipcv1 "github.com/darkliquid/tilbo/internal/ipc/gen/tilbo/ipc/v1"
"github.com/darkliquid/tilbo/internal/rules"
isync "github.com/darkliquid/tilbo/internal/sync"
"github.com/darkliquid/tilbo/internal/uisocket"
"github.com/darkliquid/tilbo/internal/vectorize"
"github.com/darkliquid/tilbo/internal/watcher"
"github.com/darkliquid/tilbo/internal/xattr"
Expand Down Expand Up @@ -57,6 +56,8 @@ func main() {

// run is the main daemon loop. It returns nil on clean shutdown and a non-nil
// error if any component fails unexpectedly.
//
//nolint:funlen // daemon initialization is inherently long
func run(
ctx context.Context,
hupCh <-chan os.Signal,
Expand Down Expand Up @@ -104,8 +105,12 @@ func run(

proc := newProcessor(idx, tags, pipeline, engine, fileGraph, embedder)

uiSrv := initUIServerSocket(idx, tags, fileGraph, fuseMount, proc, syncer)
startUISocketListener(ctx, uiSrv)
uiBrowserMethods := &daemonBrowserMethods{
idx: idx,
tags: tags,
g: fileGraph,
fuseMount: fuseMount,
}

sweeper := rules.NewSweeper(idx, tags, pipeline, engine)

Expand All @@ -130,6 +135,7 @@ func run(
cfgPath,
engine,
embedder,
uiBrowserMethods,
)

ipcServer, err := startIPCServer(ctx, sockPath, handleIPCRequest)
Expand All @@ -138,43 +144,34 @@ func run(
}
defer ipcServer.Stop()

slog.InfoContext(ctx, "tilbo-daemon ready", "socket", sockPath)
startFuseMount(ctx, fuseMount, idx, fileGraph, warningStore.Append)

return runEventLoop(ctx, events, watchErrCh, hupCh, syncer, idx, proc, engine, ruleReg, sweeper, wasmCache, cfgPath)
}

func initUIServerSocket(
idx *index.DB,
tags *xattr.Service,
fileGraph *graph.Graph,
fuseMount string,
proc *Processor,
syncer *isync.Syncer,
) *uisocket.Server {
uiBrowserMethods := &daemonBrowserMethods{
idx: idx,
tags: tags,
g: fileGraph,
fuseMount: fuseMount,
// onFileTagged is captured via closure; proc.OnFileTagged is set below.
onFileTagged: func(path string, added, removed []string) {
proc.OnFileTagged(path, added, removed)
},
}
uiSrv := uisocket.New(uiSocketPath(), uiBrowserMethods)

proc.OnFileTagged = func(path string, added, removed []string) {
uiSrv.BroadcastFileTagged(path, added, removed)
ipcServer.BroadcastEvent(&ipcv1.Event{
Kind: &ipcv1.Event_FileTagged{
FileTagged: &ipcv1.FileTaggedEvent{Path: path, Added: added, Removed: removed},
},
})
}
uiBrowserMethods.onFileTagged = proc.OnFileTagged

syncer.OnStateChanged = func(state ipcv1.DaemonState) {
uiSrv.BroadcastDaemonStateChanged(daemonStateLabel(state))
ipcServer.BroadcastEvent(&ipcv1.Event{
Kind: &ipcv1.Event_DaemonStateChanged{
DaemonStateChanged: &ipcv1.DaemonStateChangedEvent{State: daemonStateLabel(state)},
},
})
}
syncer.OnIndexUpdated = func(filesTotal, tagsTotal uint64) {
uiSrv.BroadcastIndexUpdated(filesTotal, tagsTotal)
ipcServer.BroadcastEvent(&ipcv1.Event{
Kind: &ipcv1.Event_IndexUpdated{
IndexUpdated: &ipcv1.IndexUpdatedEvent{FilesTotal: filesTotal, TagsTotal: tagsTotal},
},
})
}

return uiSrv
slog.InfoContext(ctx, "tilbo-daemon ready", "socket", sockPath)
startFuseMount(ctx, fuseMount, idx, fileGraph, warningStore.Append)

return runEventLoop(ctx, events, watchErrCh, hupCh, syncer, idx, proc, engine, ruleReg, sweeper, wasmCache, cfgPath)
}

func ensureParentDir(path string, action string) error {
Expand Down Expand Up @@ -297,14 +294,6 @@ func initOptionalEmbedder(
return embedder
}

func startUISocketListener(ctx context.Context, uiSrv *uisocket.Server) {
go func() {
if err := uiSrv.Listen(ctx); err != nil {
slog.WarnContext(ctx, "uisocket: stopped", "err", err)
}
}()
}

func startSyncerLoop(ctx context.Context, syncer *isync.Syncer, proc *Processor) <-chan error {
syncer.OnFileSynced = proc.ProcessFile

Expand Down Expand Up @@ -407,6 +396,7 @@ func buildIPCRequestHandler(
cfgPath string,
engine *rules.Engine,
embedder *vectorize.ONNXEmbedder,
browser *daemonBrowserMethods,
) func(context.Context, *ipcv1.Request) (*ipcv1.Response, error) {
return func(ctx context.Context, req *ipcv1.Request) (*ipcv1.Response, error) {
switch r := req.GetKind().(type) {
Expand Down Expand Up @@ -443,6 +433,21 @@ func buildIPCRequestHandler(
case *ipcv1.Request_HydrateTags:
return handleHydrateTags(ctx, r.HydrateTags, idx)

case *ipcv1.Request_ListDirectory:
return handleListDirectory(r.ListDirectory, browser)
case *ipcv1.Request_StatFile:
return handleStatFile(r.StatFile, browser)
case *ipcv1.Request_GlobSearch:
return handleGlobSearch(r.GlobSearch, browser)
case *ipcv1.Request_RenameFile:
return handleRenameFile(r.RenameFile, browser)
case *ipcv1.Request_DeleteFile:
return handleDeleteFile(r.DeleteFile, browser)
case *ipcv1.Request_ChmodFile:
return handleChmodFile(r.ChmodFile, browser)
case *ipcv1.Request_ListPlaces:
return handleListPlaces(r.ListPlaces, browser)

case *ipcv1.Request_ReloadRules:
var reloadErrs []string
engine.Reset()
Expand Down Expand Up @@ -826,16 +831,6 @@ func socketPath() string {
return fmt.Sprintf("/run/user/%d/tilbo.sock", uid)
}

// uiSocketPath returns the well-known Unix socket path for the Quickshell UI
// frontend. It always derives from XDG_RUNTIME_DIR (or /run/user/<uid>) so it
// is stable and independent of any IPC socket path override.
func uiSocketPath() string {
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
return filepath.Join(dir, "tilbo-ui.sock")
}
return fmt.Sprintf("/run/user/%d/tilbo-ui.sock", os.Getuid())
}

// defaultFuseMountPath returns the default FUSE mount point for the current user.
func defaultFuseMountPath() string {
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
Expand Down
Loading
Loading