/* rigel.c — OrionOS File Manager * Named after Rigel, the brightest star in Orion. * Native C/Xlib + SkyKit. No GTK/Qt dependency. * * Layout: * [toolbar: ← → ↑ /path/bar ] * [sidebar | Name ↑ Size Modified ] * [ | DIR folder - Jul 23 ] * [ | TXT file.txt 4 KB Jul 22 ] * [ | IMG photo.jpg 2 MB Jul 10 ] * [ Home | ] * [ Docs |────────────────────────────────────── ] * [ Down | 47 items [search bar when active] ] */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../skykit/skykit.h" /* ── Layout ──────────────────────────────────────────────────────── */ #define WIN_W 920 #define WIN_H 600 #define SIDE_W 180 #define TOOL_H 40 #define COL_H 22 #define STAT_H 24 #define ROW_H 24 #define FILE_X (SIDE_W + 1) #define FILE_W (WIN_W - SIDE_W - 1) #define LIST_Y (TOOL_H + COL_H) #define LIST_H (WIN_H - TOOL_H - COL_H - STAT_H) #define LIST_ROWS (LIST_H / ROW_H) /* columns within the file area (relative to FILE_X) */ #define BADGE_W 44 #define DATE_W 130 #define SIZE_W 80 #define SB_W 8 #define NAME_W (FILE_W - BADGE_W - SIZE_W - DATE_W - SB_W - 2) /* sidebar */ #define SIDE_HDR_H 36 #define SIDE_ITEM_H 28 /* toolbar */ #define BTN_W 26 #define BTN_H 24 #define BACK_X 8 #define FWD_X (BACK_X + BTN_W + 4) #define UP_X (FWD_X + BTN_W + 4) #define PATH_X (UP_X + BTN_W + 10) #define PATH_W (WIN_W - PATH_X - 8) #define PATH_H 26 /* context menu */ #define CTX_W 200 #define CTX_IH 24 #define CTX_SH 8 #define CTX_PAD 4 /* ── File types ───────────────────────────────────────────────────── */ typedef enum { FT_DIR, FT_IMAGE, FT_AUDIO, FT_VIDEO, FT_ARCHIVE, FT_CODE, FT_PDF, FT_TEXT, FT_EXEC, FT_UNKNOWN, FT__N } FileType; static const struct { unsigned long bg; const char *lbl; unsigned long fg; } BADGE[FT__N] = { [FT_DIR] = { 0x1A4888, "DIR", 0xA0C8F8 }, [FT_IMAGE] = { 0x5A3088, "IMG", 0xCCA8F8 }, [FT_AUDIO] = { 0x1A6038, "SND", 0x80E0A8 }, [FT_VIDEO] = { 0x782020, "VID", 0xF88080 }, [FT_ARCHIVE] = { 0x6A3808, "ZIP", 0xF0A060 }, [FT_CODE] = { 0x0A5868, "SRC", 0x60D8E8 }, [FT_PDF] = { 0x882010, "PDF", 0xF87060 }, [FT_TEXT] = { 0x203040, "TXT", 0x80A0C0 }, [FT_EXEC] = { 0x1A6828, "EXE", 0x80F890 }, [FT_UNKNOWN] = { 0x1A2030, "---", 0x506070 }, }; static FileType detect_type(const char *name, int is_dir, int is_exec) { if (is_dir) return FT_DIR; if (is_exec) return FT_EXEC; const char *e = strrchr(name, '.'); if (!e) return FT_UNKNOWN; e++; #define EQ(a) (strcasecmp(e,(a))==0) if (EQ("jpg")||EQ("jpeg")||EQ("png")||EQ("gif")||EQ("bmp")||EQ("webp")||EQ("svg")||EQ("tiff")) return FT_IMAGE; if (EQ("mp3")||EQ("flac")||EQ("ogg")||EQ("wav")||EQ("aac")||EQ("m4a")||EQ("opus")) return FT_AUDIO; if (EQ("mp4")||EQ("mkv")||EQ("avi")||EQ("mov")||EQ("webm")||EQ("flv")||EQ("m4v")) return FT_VIDEO; if (EQ("zip")||EQ("tar")||EQ("gz")||EQ("xz")||EQ("bz2")||EQ("7z")||EQ("rar")||EQ("tgz")) return FT_ARCHIVE; if (EQ("c")||EQ("h")||EQ("cpp")||EQ("py")||EQ("js")||EQ("ts")||EQ("rs")||EQ("go") ||EQ("sh")||EQ("bash")||EQ("java")||EQ("rb")||EQ("css")||EQ("html")||EQ("xml") ||EQ("json")||EQ("toml")||EQ("yaml")||EQ("yml")) return FT_CODE; if (EQ("pdf")) return FT_PDF; if (EQ("txt")||EQ("md")||EQ("rst")||EQ("log")||EQ("csv")) return FT_TEXT; #undef EQ return FT_UNKNOWN; } /* ── File entry ───────────────────────────────────────────────────── */ #define FMAX 4096 typedef struct { char name[512]; off_t size; time_t mtime; FileType type; } FileEntry; /* ── Bookmarks ────────────────────────────────────────────────────── */ static const struct { const char *name; const char *path; } BKMARKS[] = { { "Home", "~" }, { "Desktop", "~/Desktop" }, { "Documents", "~/Documents" }, { "Downloads", "~/Downloads" }, { "Pictures", "~/Pictures" }, { "Music", "~/Music" }, { "Videos", "~/Videos" }, { "Trash", "~/.local/share/Trash/files" }, }; #define NBKMARKS ((int)(sizeof(BKMARKS)/sizeof(BKMARKS[0]))) /* ── Context menu items ───────────────────────────────────────────── */ typedef struct { const char *lbl; int sep; } CtxItem; static const CtxItem CTX[] = { { "Open", 0 }, { "Open in Quill", 0 }, { "Open in Terminal", 0 }, { NULL, 1 }, { "Copy Path", 0 }, { "Rename", 0 }, { "Move to Trash", 0 }, { NULL, 1 }, { "Properties", 0 }, }; #define CTX_N ((int)(sizeof(CTX)/sizeof(CTX[0]))) /* ── App struct ───────────────────────────────────────────────────── */ #define HIST_MAX 64 typedef struct { SKApp sk; Window win; GC gc; Pixmap buf; SKPainter p; Atom wm_delete; int running; char cwd[4096]; char history[HIST_MAX][4096]; int hist_pos; int hist_len; FileEntry files[FMAX]; int nfiles; int scroll; int hover_row; /* files[] index, -1 if none */ int sel_row; /* files[] index, -1 if none */ int show_hidden; int sort_col; /* 0=name 1=size 2=date */ int sort_dir; /* +1=asc -1=desc */ int sidebar_hover; int hover_btn; /* 1=back 2=fwd 3=up */ char path_buf[4096]; int path_editing; int path_cursor; Time last_click_time; int last_click_row; /* files[] index */ /* ── search / filter ──────────────────────────── */ char search_str[256]; int search_len; int search_on; int filtered[FMAX]; /* files[] indices */ int nfiltered; /* ── right-click context menu ─────────────────── */ Window ctx_win; int ctx_vis; int ctx_x, ctx_y; int ctx_file_idx; /* files[] index */ int ctx_hover; /* item index, -1 if none */ /* ── rename overlay ───────────────────────────── */ char rename_buf[512]; int rename_len; int rename_on; int rename_idx; /* files[] index */ } Rigel; /* ── Path helpers ─────────────────────────────────────────────────── */ static void expand_path(const char *in, char *out, int len) { const char *home = getenv("HOME"); if (!home) home = "/"; if (in[0] == '~') { snprintf(out, len, "%s%s", home, in + 1); } else { snprintf(out, len, "%s", in); } } static void fmt_size(off_t sz, char *buf, int len) { if (sz < 0) snprintf(buf, len, "—"); else if (sz < 1024) snprintf(buf, len, "%lld B", (long long)sz); else if (sz < 1<<20) snprintf(buf, len, "%.1f KB", sz / 1024.0); else if (sz < (1<<20)*1024LL) snprintf(buf, len, "%.1f MB", sz / (1024.0*1024)); else snprintf(buf, len, "%.1f GB", sz / (1024.0*1024*1024)); } static void fmt_date(time_t t, char *buf, int len) { static const char *MO[] = { "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec" }; struct tm *tm = localtime(&t); if (!tm) { snprintf(buf, len, "—"); return; } snprintf(buf, len, "%s %2d, %4d", MO[tm->tm_mon], tm->tm_mday, 1900+tm->tm_year); } /* ── Sort ────────────────────────────────────────────────────────── */ static int g_sort_col, g_sort_dir; static int cmp_files(const void *a, const void *b) { const FileEntry *fa = (const FileEntry *)a; const FileEntry *fb = (const FileEntry *)b; int da = (fa->type == FT_DIR), db = (fb->type == FT_DIR); if (da != db) return da ? -1 : 1; int r = 0; switch (g_sort_col) { case 1: r = (fa->size > fb->size) - (fa->size < fb->size); break; case 2: r = (fa->mtime > fb->mtime) - (fa->mtime < fb->mtime); break; default: r = strcasecmp(fa->name, fb->name); break; } return r * g_sort_dir; } /* ── Directory listing ───────────────────────────────────────────── */ static void list_dir(Rigel *r) { r->nfiles = 0; r->scroll = 0; r->sel_row = -1; r->hover_row = -1; r->search_on = 0; r->search_len = 0; r->search_str[0] = '\0'; DIR *dir = opendir(r->cwd); if (!dir) return; struct dirent *ent; while ((ent = readdir(dir)) && r->nfiles < FMAX) { const char *n = ent->d_name; if (strcmp(n, ".") == 0 || strcmp(n, "..") == 0) continue; if (!r->show_hidden && n[0] == '.') continue; char full[4096*2]; snprintf(full, sizeof(full), "%s/%s", r->cwd, n); struct stat st; if (stat(full, &st) != 0) continue; FileEntry *fe = &r->files[r->nfiles++]; snprintf(fe->name, sizeof(fe->name), "%s", n); fe->size = S_ISDIR(st.st_mode) ? -1 : st.st_size; fe->mtime = st.st_mtime; fe->type = detect_type(n, S_ISDIR(st.st_mode), !S_ISDIR(st.st_mode) && (st.st_mode & S_IXUSR)); } closedir(dir); g_sort_col = r->sort_col; g_sort_dir = r->sort_dir; qsort(r->files, r->nfiles, sizeof(FileEntry), cmp_files); } /* ── Search filter ───────────────────────────────────────────────── */ static void search_filter(Rigel *r) { r->nfiltered = 0; r->scroll = 0; r->sel_row = -1; if (!r->search_str[0]) return; for (int i = 0; i < r->nfiles; i++) { if (strcasestr(r->files[i].name, r->search_str)) r->filtered[r->nfiltered++] = i; } } static int get_nvis(Rigel *r) { return r->search_on ? r->nfiltered : r->nfiles; } /* translate visual row (vis_idx) → files[] index */ static int get_fidx(Rigel *r, int vis_idx) { if (r->search_on) { if (vis_idx < 0 || vis_idx >= r->nfiltered) return -1; return r->filtered[vis_idx]; } if (vis_idx < 0 || vis_idx >= r->nfiles) return -1; return vis_idx; } /* ── Navigation ──────────────────────────────────────────────────── */ static void nav_cd(Rigel *r, const char *path) { char real[4096]; if (realpath(path, real)) { snprintf(r->cwd, sizeof(r->cwd), "%s", real); } else { snprintf(r->cwd, sizeof(r->cwd), "%s", path); } if (r->hist_len > 0 && strcmp(r->history[r->hist_pos], r->cwd) == 0) goto done; r->hist_pos++; if (r->hist_pos >= HIST_MAX) { memmove(r->history[0], r->history[1], (HIST_MAX-1) * sizeof(r->history[0])); r->hist_pos = HIST_MAX - 1; } snprintf(r->history[r->hist_pos], sizeof(r->history[0]), "%s", r->cwd); r->hist_len = r->hist_pos + 1; done: list_dir(r); snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); } static void nav_back(Rigel *r) { if (r->hist_pos <= 0) return; r->hist_pos--; snprintf(r->cwd, sizeof(r->cwd), "%s", r->history[r->hist_pos]); list_dir(r); snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); } static void nav_fwd(Rigel *r) { if (r->hist_pos >= r->hist_len - 1) return; r->hist_pos++; snprintf(r->cwd, sizeof(r->cwd), "%s", r->history[r->hist_pos]); list_dir(r); snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); } static void nav_up(Rigel *r) { char parent[4096]; snprintf(parent, sizeof(parent), "%s", r->cwd); char *sl = strrchr(parent, '/'); if (!sl || sl == parent) nav_cd(r, "/"); else { *sl = '\0'; nav_cd(r, parent); } } static void open_entry(Rigel *r, int fidx) { if (fidx < 0 || fidx >= r->nfiles) return; FileEntry *fe = &r->files[fidx]; if (fe->type == FT_DIR) { char path[4096*2]; snprintf(path, sizeof(path), "%s/%s", r->cwd, fe->name); nav_cd(r, path); } else { char cmd[4096*2 + 32]; snprintf(cmd, sizeof(cmd), "xdg-open \"%s/%s\" 2>/dev/null &", r->cwd, fe->name); system(cmd); /* intentional: user-triggered open */ } } /* ── Context menu ────────────────────────────────────────────────── */ static int ctx_item_h(int i) { return CTX[i].sep ? CTX_SH : CTX_IH; } static int ctx_total_h(void) { int h = CTX_PAD * 2; for (int i = 0; i < CTX_N; i++) h += ctx_item_h(i); return h; } static int ctx_item_y(int idx) { int y = CTX_PAD; for (int i = 0; i < idx; i++) y += ctx_item_h(i); return y; } static int ctx_item_at(int py) { int y = CTX_PAD; for (int i = 0; i < CTX_N; i++) { int h = ctx_item_h(i); if (!CTX[i].sep && py >= y && py < y + h) return i; y += h; } return -1; } static void ctx_hide(Rigel *r) { if (!r->ctx_vis) return; XUngrabPointer(r->sk.dpy, CurrentTime); XDestroyWindow(r->sk.dpy, r->ctx_win); r->ctx_vis = 0; r->ctx_win = 0; r->ctx_hover = -1; } static void ctx_draw(Rigel *r) { if (!r->ctx_vis) return; int ch = ctx_total_h(); XftDraw *xd = XftDrawCreate(r->sk.dpy, r->ctx_win, r->sk.vis, r->sk.cmap); XSetForeground(r->sk.dpy, r->gc, sk_color(&r->sk, 0x1A2030UL)); XFillRectangle(r->sk.dpy, r->ctx_win, r->gc, 0, 0, CTX_W, ch); for (int i = 0; i < CTX_N; i++) { int iy = ctx_item_y(i); if (CTX[i].sep) { XSetForeground(r->sk.dpy, r->gc, sk_color(&r->sk, 0x2A3040UL)); XDrawLine(r->sk.dpy, r->ctx_win, r->gc, 8, iy + 4, CTX_W - 8, iy + 4); continue; } if (i == r->ctx_hover) { XSetForeground(r->sk.dpy, r->gc, sk_color(&r->sk, 0x264878UL)); XFillRectangle(r->sk.dpy, r->ctx_win, r->gc, 2, iy, CTX_W - 4, CTX_IH); } unsigned long fg = (i == r->ctx_hover) ? 0xF0F4FFUL : 0xC8D4E8UL; XftColor col; XRenderColor rc = { .red = ((fg >> 16) & 0xFF) * 257, .green = ((fg >> 8) & 0xFF) * 257, .blue = ( fg & 0xFF) * 257, .alpha = 0xFFFF }; XftColorAllocValue(r->sk.dpy, r->sk.vis, r->sk.cmap, &rc, &col); int ty = iy + (CTX_IH + r->sk.font_ui->ascent - r->sk.font_ui->descent) / 2; XftDrawStringUtf8(xd, &col, r->sk.font_ui, CTX_PAD + 4, ty, (const FcChar8 *)CTX[i].lbl, (int)strlen(CTX[i].lbl)); XftColorFree(r->sk.dpy, r->sk.vis, r->sk.cmap, &col); } XftDrawDestroy(xd); XFlush(r->sk.dpy); } static void ctx_show(Rigel *r, int x, int y, int fidx) { if (r->ctx_vis) ctx_hide(r); r->ctx_file_idx = fidx; r->ctx_hover = -1; int sw = DisplayWidth(r->sk.dpy, r->sk.screen); int sh = DisplayHeight(r->sk.dpy, r->sk.screen); int ch = ctx_total_h(); int wx, wy; Window dummy; XTranslateCoordinates(r->sk.dpy, r->win, r->sk.root, x, y, &wx, &wy, &dummy); if (wx + CTX_W > sw) wx = sw - CTX_W - 2; if (wy + ch > sh) wy = sh - ch - 2; r->ctx_x = wx; r->ctx_y = wy; XSetWindowAttributes swa = { .override_redirect = True, .background_pixel = sk_color(&r->sk, 0x1A2030UL), .event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | LeaveWindowMask, .save_under = True, }; r->ctx_win = XCreateWindow(r->sk.dpy, r->sk.root, wx, wy, CTX_W, ch, 1, r->sk.depth, InputOutput, r->sk.vis, CWOverrideRedirect | CWBackPixel | CWEventMask | CWSaveUnder, &swa); XSetWindowBorder(r->sk.dpy, r->ctx_win, sk_color(&r->sk, SK_BORDER)); XMapRaised(r->sk.dpy, r->ctx_win); XGrabPointer(r->sk.dpy, r->ctx_win, False, ButtonPressMask | ButtonReleaseMask | PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime); r->ctx_vis = 1; ctx_draw(r); } static void ctx_exec(Rigel *r, int item) { int fidx = r->ctx_file_idx; if (fidx < 0 || fidx >= r->nfiles) return; FileEntry *fe = &r->files[fidx]; char full[4096 + 512]; snprintf(full, sizeof(full), "%s/%s", r->cwd, fe->name); char cmd[sizeof(full) + 256]; switch (item) { case 0: /* Open */ open_entry(r, fidx); break; case 1: /* Open in Quill */ snprintf(cmd, sizeof(cmd), "quill \"%s\" &", full); system(cmd); break; case 2: /* Open in Terminal */ snprintf(cmd, sizeof(cmd), "alacritty --working-directory \"%s\" &", fe->type == FT_DIR ? full : r->cwd); system(cmd); break; case 4: /* Copy Path */ snprintf(cmd, sizeof(cmd), "printf '%%s' \"%s\" | xclip -selection clipboard 2>/dev/null" " || printf '%%s' \"%s\" | xsel --clipboard --input 2>/dev/null", full, full); system(cmd); break; case 5: /* Rename */ snprintf(r->rename_buf, sizeof(r->rename_buf), "%s", fe->name); r->rename_len = (int)strlen(r->rename_buf); r->rename_idx = fidx; r->rename_on = 1; break; case 6: /* Move to Trash */ snprintf(cmd, sizeof(cmd), "gio trash \"%s\" 2>/dev/null" " || mv \"%s\" ~/.local/share/Trash/files/ 2>/dev/null", full, full); system(cmd); list_dir(r); break; case 8: /* Properties */ { struct stat st; if (stat(full, &st) == 0) { char sbuf[32]; fmt_size(st.st_size, sbuf, sizeof(sbuf)); char dbuf[32]; fmt_date(st.st_mtime, dbuf, sizeof(dbuf)); char msg[1024]; snprintf(msg, sizeof(msg), "Name: %s\nPath: %s\nSize: %s\nModified: %s\nMode: %o", fe->name, full, sbuf, dbuf, (unsigned)st.st_mode); snprintf(cmd, sizeof(cmd), "alacritty -T 'Properties' -e bash -c " "'echo \"%s\"; read -p \"Press Enter to close...\"' &", msg); system(cmd); } } break; } } /* ── Drawing helpers ─────────────────────────────────────────────── */ static void draw_flush(Rigel *r) { XCopyArea(r->sk.dpy, r->buf, r->win, r->gc, 0, 0, WIN_W, WIN_H, 0, 0); XFlush(r->sk.dpy); } static void draw_toolbar(Rigel *r) { sk_fill(&r->p, SK_SURFACE, 0, 0, WIN_W, TOOL_H); sk_line(&r->p, SK_BORDER, 0, TOOL_H-1, WIN_W, TOOL_H-1); int can_back = r->hist_pos > 0; int can_fwd = r->hist_pos < r->hist_len - 1; int by = (TOOL_H - BTN_H) / 2; struct { int id; int x; const char *lbl; int active; } btns[] = { { 1, BACK_X, "←", can_back }, { 2, FWD_X, "→", can_fwd }, { 3, UP_X, "↑", 1 }, }; for (int i = 0; i < 3; i++) { unsigned long bg = btns[i].active ? SK_BTN : SK_PANEL; unsigned long bgh = btns[i].active ? SK_BTN_HOV : SK_PANEL; unsigned long fg = btns[i].active ? SK_TEXT_HDR : SK_TEXT_DIM; int hov = (r->hover_btn == btns[i].id && btns[i].active); sk_fill(&r->p, hov ? bgh : bg, btns[i].x, by, BTN_W, BTN_H); sk_rect(&r->p, SK_BORDER, btns[i].x, by, BTN_W, BTN_H); int tx = btns[i].x + (BTN_W - sk_textw(&r->sk, r->sk.font_ui, btns[i].lbl)) / 2; sk_text(&r->p, r->sk.font_ui, fg, tx, sk_baseline(r->sk.font_ui, by, BTN_H), btns[i].lbl); } /* path bar */ int px = PATH_X, pw = PATH_W, ph = PATH_H, py = (TOOL_H - ph) / 2; unsigned long pbg = r->path_editing ? SK_PANEL : SK_BG; sk_fill(&r->p, pbg, px, py, pw, ph); sk_rect(&r->p, r->path_editing ? SK_ACCENT : SK_BORDER, px, py, pw, ph); const char *ptext = r->path_editing ? r->path_buf : r->cwd; sk_text_clip(&r->p, r->sk.font_ui, SK_TEXT_HDR, px + 6, sk_baseline(r->sk.font_ui, py, ph), ptext, px+1, py+1, pw-2, ph-2); if (r->path_editing) { char tmp[4096]; int cpos = r->path_cursor; if (cpos > (int)strlen(r->path_buf)) cpos = (int)strlen(r->path_buf); snprintf(tmp, sizeof(tmp), "%.*s", cpos, r->path_buf); int cx = px + 6 + sk_textw(&r->sk, r->sk.font_ui, tmp); sk_fill(&r->p, SK_TEXT, cx, py + 4, 1, ph - 8); } } static void draw_sidebar(Rigel *r) { sk_fill(&r->p, SK_SIDEBAR, 0, 0, SIDE_W, WIN_H); sk_line(&r->p, SK_BORDER, SIDE_W, 0, SIDE_W, WIN_H); sk_fill(&r->p, 0x06090FUL, 0, TOOL_H, SIDE_W, SIDE_HDR_H); sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, 12, sk_baseline(r->sk.font_sm, TOOL_H, SIDE_HDR_H), "BOOKMARKS"); sk_line(&r->p, SK_BORDER, 0, TOOL_H + SIDE_HDR_H - 1, SIDE_W, TOOL_H + SIDE_HDR_H - 1); int iy = TOOL_H + SIDE_HDR_H; for (int i = 0; i < NBKMARKS; i++, iy += SIDE_ITEM_H) { int hov = (r->sidebar_hover == i); char expanded[4096]; expand_path(BKMARKS[i].path, expanded, sizeof(expanded)); int active = (strcmp(expanded, r->cwd) == 0); unsigned long bg = active ? SK_SEL : (hov ? SK_ROW_HOV : SK_SIDEBAR); sk_fill(&r->p, bg, 0, iy, SIDE_W, SIDE_ITEM_H); if (active) sk_fill(&r->p, SK_ACCENT, 0, iy, 3, SIDE_ITEM_H); unsigned long fg = active ? SK_TEXT_HDR : (hov ? SK_TEXT_HOV : SK_TEXT); sk_text(&r->p, r->sk.font_ui, fg, active ? 16 : 12, sk_baseline(r->sk.font_ui, iy, SIDE_ITEM_H), BKMARKS[i].name); } } static void draw_col_header(Rigel *r) { int hy = TOOL_H; sk_fill(&r->p, 0x0A1020UL, FILE_X, hy, FILE_W, COL_H); sk_line(&r->p, SK_BORDER, FILE_X, hy + COL_H - 1, WIN_W, hy + COL_H - 1); struct { int x; int w; const char *lbl; int col; } cols[] = { { FILE_X + BADGE_W, NAME_W, "Name", 0 }, { FILE_X + BADGE_W + NAME_W, SIZE_W, "Size", 1 }, { FILE_X + BADGE_W + NAME_W + SIZE_W, DATE_W, "Modified", 2 }, }; for (int i = 0; i < 3; i++) { unsigned long fc = (r->sort_col == cols[i].col) ? SK_TEXT_HDR : SK_TEXT_DIM; sk_text(&r->p, r->sk.font_sm, fc, cols[i].x + 4, sk_baseline(r->sk.font_sm, hy, COL_H), cols[i].lbl); if (r->sort_col == cols[i].col) { const char *arr = (r->sort_dir > 0) ? "▲" : "▼"; sk_text(&r->p, r->sk.font_sm, SK_ACCENT, cols[i].x + 4 + sk_textw(&r->sk, r->sk.font_sm, cols[i].lbl) + 4, sk_baseline(r->sk.font_sm, hy, COL_H), arr); } } } static void draw_filelist(Rigel *r) { sk_fill(&r->p, SK_BG, FILE_X, LIST_Y, FILE_W, LIST_H); int nvis = get_nvis(r); int visible = nvis - r->scroll; if (visible > LIST_ROWS) visible = LIST_ROWS; for (int i = 0; i < visible; i++) { int vis_idx = i + r->scroll; int ry = LIST_Y + i * ROW_H; int fidx = get_fidx(r, vis_idx); FileEntry *fe = (fidx >= 0) ? &r->files[fidx] : NULL; if (!fe) break; unsigned long bg = SK_BG; if (fidx == r->sel_row) bg = SK_SEL; else if (fidx == r->hover_row) bg = SK_ROW_HOV; else if (i % 2 == 1) bg = SK_ROW_ALT; sk_fill(&r->p, bg, FILE_X, ry, FILE_W - SB_W, ROW_H); /* type badge */ int bx = FILE_X + 4, badgew = BADGE_W - 8; int badgeh = ROW_H - 6, badgey = ry + 3; sk_fill(&r->p, BADGE[fe->type].bg, bx, badgey, badgew, badgeh); int lx = bx + (badgew - sk_textw(&r->sk, r->sk.font_sm, BADGE[fe->type].lbl)) / 2; sk_text(&r->p, r->sk.font_sm, BADGE[fe->type].fg, lx, sk_baseline(r->sk.font_sm, badgey, badgeh), BADGE[fe->type].lbl); /* name */ int ty = sk_baseline(r->sk.font_ui, ry, ROW_H); unsigned long fg = (fidx == r->sel_row || fidx == r->hover_row) ? SK_TEXT_HDR : SK_TEXT; if (r->search_on && r->search_str[0]) { char *match = strcasestr(fe->name, r->search_str); if (match) { /* accent rect behind the matching substring */ int pre_len = (int)(match - fe->name); int match_len = r->search_len; int nx = FILE_X + BADGE_W + 2; char tmp[512]; snprintf(tmp, sizeof(tmp), "%.*s", pre_len, fe->name); int mx = nx + sk_textw(&r->sk, r->sk.font_ui, tmp); snprintf(tmp, sizeof(tmp), "%.*s", match_len, match); int mw = sk_textw(&r->sk, r->sk.font_ui, tmp); sk_fill(&r->p, SK_ACCENT, mx, ry + 2, mw, ROW_H - 4); } } sk_text_clip(&r->p, r->sk.font_ui, fg, FILE_X + BADGE_W + 2, ty, fe->name, FILE_X + BADGE_W, ry, NAME_W, ROW_H); /* size */ char sbuf[32]; fmt_size(fe->size, sbuf, sizeof(sbuf)); int sx = FILE_X + BADGE_W + NAME_W + 4; sk_text_clip(&r->p, r->sk.font_ui, SK_TEXT_DIM, sx, ty, sbuf, sx, ry, SIZE_W - 8, ROW_H); /* date */ char dbuf[32]; fmt_date(fe->mtime, dbuf, sizeof(dbuf)); int dx = FILE_X + BADGE_W + NAME_W + SIZE_W + 4; sk_text_clip(&r->p, r->sk.font_ui, SK_TEXT_DIM, dx, ty, dbuf, dx, ry, DATE_W - 4, ROW_H); sk_line(&r->p, SK_SEP, FILE_X, ry + ROW_H - 1, WIN_W - SB_W, ry + ROW_H - 1); } /* scrollbar */ if (nvis > LIST_ROWS) { int sbx = WIN_W - SB_W; sk_fill(&r->p, SK_PANEL, sbx, LIST_Y, SB_W, LIST_H); int th = LIST_H * LIST_ROWS / nvis; if (th < 20) th = 20; int ty2 = LIST_Y + (LIST_H - th) * r->scroll / (nvis - LIST_ROWS); sk_fill(&r->p, SK_ACCENT, sbx, ty2, SB_W, th); } } static void draw_statusbar(Rigel *r) { int sy = WIN_H - STAT_H; sk_fill(&r->p, SK_SURFACE, 0, sy, WIN_W, STAT_H); sk_line(&r->p, SK_BORDER, 0, sy, WIN_W, sy); if (r->search_on) { /* show search bar */ sk_fill(&r->p, 0x0A1020UL, FILE_X, sy, FILE_W, STAT_H); sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, FILE_X + 8, sk_baseline(r->sk.font_sm, sy, STAT_H), "Filter: "); int pfx = FILE_X + 8 + sk_textw(&r->sk, r->sk.font_sm, "Filter: "); if (r->search_len > 0) sk_text(&r->p, r->sk.font_ui, SK_TEXT_HDR, pfx, sk_baseline(r->sk.font_ui, sy, STAT_H), r->search_str); /* cursor */ int cx = pfx + sk_textw(&r->sk, r->sk.font_ui, r->search_str); sk_fill(&r->p, SK_ACCENT, cx, sy + 4, 2, STAT_H - 8); /* result count */ char cnt[32]; snprintf(cnt, sizeof(cnt), "%d match%s Esc: clear", r->nfiltered, r->nfiltered == 1 ? "" : "es"); sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, WIN_W - sk_textw(&r->sk, r->sk.font_sm, cnt) - 10, sk_baseline(r->sk.font_sm, sy, STAT_H), cnt); } else { char msg[128]; if (r->sel_row >= 0 && r->sel_row < r->nfiles) { char sb[32]; fmt_size(r->files[r->sel_row].size, sb, sizeof(sb)); snprintf(msg, sizeof(msg), "%d items — \"%s\" %s", r->nfiles, r->files[r->sel_row].name, r->files[r->sel_row].type == FT_DIR ? "(folder)" : sb); } else { snprintf(msg, sizeof(msg), "%d item%s", r->nfiles, r->nfiles==1?"":"s"); } sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, 10, sk_baseline(r->sk.font_sm, sy, STAT_H), msg); if (r->show_hidden) { const char *hint = "· showing hidden Ctrl+F: filter"; sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, WIN_W - sk_textw(&r->sk, r->sk.font_sm, hint) - 10, sk_baseline(r->sk.font_sm, sy, STAT_H), hint); } else { const char *hint = "Ctrl+F: filter"; sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, WIN_W - sk_textw(&r->sk, r->sk.font_sm, hint) - 10, sk_baseline(r->sk.font_sm, sy, STAT_H), hint); } } } static void draw_rename_overlay(Rigel *r) { if (!r->rename_on) return; int ry = WIN_H - STAT_H - 34; sk_fill(&r->p, 0x0A1020UL, FILE_X, ry, FILE_W, 32); sk_rect(&r->p, SK_ACCENT, FILE_X, ry, FILE_W, 32); sk_text(&r->p, r->sk.font_sm, SK_TEXT_DIM, FILE_X + 8, sk_baseline(r->sk.font_sm, ry, 32), "Rename: "); int pfx = FILE_X + 8 + sk_textw(&r->sk, r->sk.font_sm, "Rename: "); if (r->rename_len > 0) sk_text(&r->p, r->sk.font_ui, SK_TEXT_HDR, pfx, sk_baseline(r->sk.font_ui, ry, 32), r->rename_buf); int cx = pfx + sk_textw(&r->sk, r->sk.font_ui, r->rename_buf); sk_fill(&r->p, SK_ACCENT, cx, ry + 6, 2, 20); } static void draw(Rigel *r) { draw_toolbar(r); draw_sidebar(r); draw_col_header(r); draw_filelist(r); draw_statusbar(r); draw_rename_overlay(r); draw_flush(r); if (r->ctx_vis) ctx_draw(r); } /* ── Hit testing ─────────────────────────────────────────────────── */ static int toolbar_btn_at(int x, int y) { int by = (TOOL_H - BTN_H) / 2; if (!sk_in_rect(x, y, 0, by, UP_X + BTN_W, BTN_H)) return 0; if (sk_in_rect(x, y, BACK_X, by, BTN_W, BTN_H)) return 1; if (sk_in_rect(x, y, FWD_X, by, BTN_W, BTN_H)) return 2; if (sk_in_rect(x, y, UP_X, by, BTN_W, BTN_H)) return 3; return 0; } static int file_vis_row_at(int x, int y) { if (!sk_in_rect(x, y, FILE_X, LIST_Y, FILE_W, LIST_H)) return -1; return (y - LIST_Y) / ROW_H; } static int col_header_at(int x, int y) { if (!sk_in_rect(x, y, FILE_X, TOOL_H, FILE_W, COL_H)) return -1; if (x < FILE_X + BADGE_W + NAME_W) return 0; if (x < FILE_X + BADGE_W + NAME_W + SIZE_W) return 1; return 2; } static int sidebar_item_at(int x, int y) { if (!sk_in_rect(x, y, 0, TOOL_H + SIDE_HDR_H, SIDE_W, WIN_H)) return -1; int i = (y - TOOL_H - SIDE_HDR_H) / SIDE_ITEM_H; return (i >= 0 && i < NBKMARKS) ? i : -1; } /* ── Event handlers ──────────────────────────────────────────────── */ static void on_motion(Rigel *r, int x, int y) { int vrow = file_vis_row_at(x, y); int hrow = -1; if (vrow >= 0) { int vis_idx = vrow + r->scroll; hrow = get_fidx(r, vis_idx); } int hbtn = toolbar_btn_at(x, y); int hside = sidebar_item_at(x, y); int redraw = (hrow != r->hover_row || hbtn != r->hover_btn || hside != r->sidebar_hover); r->hover_row = hrow; r->hover_btn = hbtn; r->sidebar_hover = hside; if (redraw) draw(r); } static void on_scroll(Rigel *r, int dir) { int nvis = get_nvis(r); int max = nvis - LIST_ROWS; if (max < 0) max = 0; r->scroll += dir * 3; if (r->scroll < 0) r->scroll = 0; if (r->scroll > max) r->scroll = max; draw(r); } static void path_commit(Rigel *r) { r->path_editing = 0; char expanded[4096]; expand_path(r->path_buf, expanded, sizeof(expanded)); nav_cd(r, expanded); } static void on_click(Rigel *r, int x, int y, Time t, int btn) { /* dismiss context if open */ if (r->ctx_vis) { ctx_hide(r); draw(r); return; } /* dismiss rename if open and clicking outside */ if (r->rename_on) { r->rename_on = 0; } /* path bar */ int py = (TOOL_H - PATH_H) / 2; if (sk_in_rect(x, y, PATH_X, py, PATH_W, PATH_H)) { if (!r->path_editing) { r->path_editing = 1; snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); r->path_cursor = (int)strlen(r->path_buf); } draw(r); return; } if (r->path_editing) { r->path_editing = 0; snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); draw(r); } /* toolbar buttons (left click only) */ if (btn == Button1) { int b = toolbar_btn_at(x, y); if (b == 1) { nav_back(r); draw(r); return; } if (b == 2) { nav_fwd(r); draw(r); return; } if (b == 3) { nav_up(r); draw(r); return; } } /* sidebar */ int si = sidebar_item_at(x, y); if (si >= 0 && btn == Button1) { char expanded[4096]; expand_path(BKMARKS[si].path, expanded, sizeof(expanded)); nav_cd(r, expanded); draw(r); return; } /* column header */ int col = col_header_at(x, y); if (col >= 0 && btn == Button1) { if (r->sort_col == col) r->sort_dir = -r->sort_dir; else { r->sort_col = col; r->sort_dir = 1; } g_sort_col = r->sort_col; g_sort_dir = r->sort_dir; qsort(r->files, r->nfiles, sizeof(FileEntry), cmp_files); if (r->search_on) search_filter(r); draw(r); return; } /* file list */ int vrow = file_vis_row_at(x, y); if (vrow < 0) return; int vis_idx = vrow + r->scroll; int fidx = get_fidx(r, vis_idx); if (fidx < 0) return; if (btn == Button3) { r->sel_row = fidx; draw(r); ctx_show(r, x, y, fidx); return; } /* double-click detection */ int dbl = (fidx == r->last_click_row && (t - r->last_click_time) < 300); r->last_click_time = t; r->last_click_row = fidx; if (dbl) { open_entry(r, fidx); } else { r->sel_row = (r->sel_row == fidx) ? -1 : fidx; } draw(r); } static void on_key(Rigel *r, XKeyEvent *ke) { KeySym sym = XLookupKeysym(ke, 0); int mod = ke->state; /* ── rename overlay ────────────────────────────────────── */ if (r->rename_on) { if (sym == XK_Escape) { r->rename_on = 0; } else if (sym == XK_Return || sym == XK_KP_Enter) { /* commit rename */ if (r->rename_len > 0 && r->rename_idx >= 0 && r->rename_idx < r->nfiles) { char old_path[4096 + 512]; char new_path[4096 + 512]; snprintf(old_path, sizeof(old_path), "%s/%s", r->cwd, r->files[r->rename_idx].name); snprintf(new_path, sizeof(new_path), "%s/%s", r->cwd, r->rename_buf); rename(old_path, new_path); list_dir(r); } r->rename_on = 0; } else if (sym == XK_BackSpace) { if (r->rename_len > 0) r->rename_buf[--r->rename_len] = '\0'; } else { char buf[16]; int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL); if (n > 0 && (unsigned char)buf[0] >= 0x20) { buf[n] = '\0'; int len = (int)strlen(r->rename_buf); if (len + n < (int)sizeof(r->rename_buf) - 1) { memcpy(r->rename_buf + len, buf, n + 1); r->rename_len += n; } } } draw(r); return; } /* ── path bar editing ──────────────────────────────────── */ if (r->path_editing) { if (sym == XK_Return || sym == XK_KP_Enter) { path_commit(r); } else if (sym == XK_Escape) { r->path_editing = 0; snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); } else if (sym == XK_BackSpace) { if (r->path_cursor > 0) { int n = (int)strlen(r->path_buf); memmove(r->path_buf + r->path_cursor - 1, r->path_buf + r->path_cursor, n - r->path_cursor + 1); r->path_cursor--; } } else if (sym == XK_Left) { if (r->path_cursor > 0) r->path_cursor--; } else if (sym == XK_Right) { int n = (int)strlen(r->path_buf); if (r->path_cursor < n) r->path_cursor++; } else if (sym == XK_Home) { r->path_cursor = 0; } else if (sym == XK_End) { r->path_cursor = (int)strlen(r->path_buf); } else { char buf[16]; int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL); if (n > 0 && (unsigned char)buf[0] >= 32) { buf[n] = '\0'; int len = (int)strlen(r->path_buf); if (len + n < (int)sizeof(r->path_buf) - 1) { memmove(r->path_buf + r->path_cursor + n, r->path_buf + r->path_cursor, len - r->path_cursor + 1); memcpy(r->path_buf + r->path_cursor, buf, n); r->path_cursor += n; } } } draw(r); return; } /* ── search mode ───────────────────────────────────────── */ if (r->search_on) { if (sym == XK_Escape) { r->search_on = 0; r->search_len = 0; r->search_str[0] = '\0'; } else if (sym == XK_BackSpace) { if (r->search_len > 0) { r->search_str[--r->search_len] = '\0'; search_filter(r); } if (r->search_len == 0) { r->search_on = 0; } } else if (sym == XK_Return) { /* open first result */ if (r->nfiltered > 0) open_entry(r, r->filtered[0]); } else { char buf[8]; int n = XLookupString(ke, buf, sizeof(buf)-1, NULL, NULL); if (n > 0 && (unsigned char)buf[0] >= 0x20) { buf[n] = '\0'; if (r->search_len + n < (int)sizeof(r->search_str) - 1) { memcpy(r->search_str + r->search_len, buf, n + 1); r->search_len += n; search_filter(r); } } } draw(r); return; } /* ── normal navigation ─────────────────────────────────── */ if (sym == XK_BackSpace || (sym == XK_Left && (mod & Mod1Mask))) { nav_back(r); } else if (sym == XK_Right && (mod & Mod1Mask)) { nav_fwd(r); } else if (sym == XK_Up && (mod & Mod1Mask)) { nav_up(r); } else if (sym == XK_Up) { if (r->sel_row > 0) r->sel_row--; else if (r->sel_row < 0 && r->nfiles > 0) r->sel_row = 0; if (r->sel_row >= 0 && r->sel_row < r->scroll) r->scroll = r->sel_row; } else if (sym == XK_Down) { if (r->sel_row < r->nfiles - 1) r->sel_row++; else if (r->sel_row < 0 && r->nfiles > 0) r->sel_row = 0; if (r->sel_row >= r->scroll + LIST_ROWS) r->scroll = r->sel_row - LIST_ROWS + 1; } else if (sym == XK_Return || sym == XK_KP_Enter) { if (r->sel_row >= 0) open_entry(r, r->sel_row); } else if (sym == XK_Page_Up) { on_scroll(r, -4); return; } else if (sym == XK_Page_Down) { on_scroll(r, 4); return; } else if (sym == XK_F2) { /* rename selected */ if (r->sel_row >= 0 && r->sel_row < r->nfiles) { snprintf(r->rename_buf, sizeof(r->rename_buf), "%s", r->files[r->sel_row].name); r->rename_len = (int)strlen(r->rename_buf); r->rename_idx = r->sel_row; r->rename_on = 1; } } else if (sym == XK_Delete) { /* trash selected */ if (r->sel_row >= 0 && r->sel_row < r->nfiles) { char cmd[4096 + 512]; snprintf(cmd, sizeof(cmd), "gio trash \"%s/%s\" 2>/dev/null" " || mv \"%s/%s\" ~/.local/share/Trash/files/ 2>/dev/null", r->cwd, r->files[r->sel_row].name, r->cwd, r->files[r->sel_row].name); system(cmd); list_dir(r); } } else if (sym == XK_h && (mod & ControlMask)) { r->show_hidden = !r->show_hidden; list_dir(r); } else if ((sym == XK_f && (mod & ControlMask)) || sym == XK_F3) { r->search_on = 1; r->search_len = 0; r->search_str[0] = '\0'; search_filter(r); } else if (sym == XK_l && (mod & ControlMask)) { r->path_editing = 1; snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); r->path_cursor = (int)strlen(r->path_buf); } else if (sym == XK_slash) { r->path_editing = 1; snprintf(r->path_buf, sizeof(r->path_buf), "/"); r->path_cursor = 1; } else if (sym == XK_Escape) { r->sel_row = -1; } else if (sym == XK_q && (mod & ControlMask)) { r->running = 0; return; } draw(r); } /* ── Init ────────────────────────────────────────────────────────── */ static void init(Rigel *r) { sk_app_init(&r->sk); r->sort_col = 0; r->sort_dir = 1; r->sel_row = -1; r->hover_row = -1; r->sidebar_hover = -1; r->last_click_row = -1; r->ctx_hover = -1; const char *home = getenv("HOME"); if (!home || chdir(home) != 0) home = "/"; snprintf(r->history[0], sizeof(r->history[0]), "%s", home); r->hist_pos = 0; r->hist_len = 1; snprintf(r->cwd, sizeof(r->cwd), "%s", home); list_dir(r); snprintf(r->path_buf, sizeof(r->path_buf), "%s", r->cwd); XSetWindowAttributes swa = { .background_pixel = sk_color(&r->sk, SK_BG), .event_mask = ExposureMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | KeyPressMask | StructureNotifyMask | LeaveWindowMask, }; r->win = XCreateWindow(r->sk.dpy, r->sk.root, 80, 60, WIN_W, WIN_H, 0, r->sk.depth, InputOutput, r->sk.vis, CWBackPixel | CWEventMask, &swa); r->gc = XCreateGC(r->sk.dpy, r->win, 0, NULL); r->buf = XCreatePixmap(r->sk.dpy, r->win, WIN_W, WIN_H, r->sk.depth); r->p = (SKPainter){ .app = &r->sk, .drw = r->buf, .gc = r->gc }; XStoreName(r->sk.dpy, r->win, "Rigel — File Manager"); XClassHint *ch = XAllocClassHint(); if (ch) { ch->res_name = (char*)"rigel"; ch->res_class = (char*)"Rigel"; XSetClassHint(r->sk.dpy, r->win, ch); XFree(ch); } XSizeHints *sh = XAllocSizeHints(); if (sh) { sh->flags = PMinSize; sh->min_width = 600; sh->min_height = 400; XSetWMNormalHints(r->sk.dpy, r->win, sh); XFree(sh); } r->wm_delete = XInternAtom(r->sk.dpy, "WM_DELETE_WINDOW", False); Atom wm_prot = XInternAtom(r->sk.dpy, "WM_PROTOCOLS", False); XChangeProperty(r->sk.dpy, r->win, wm_prot, XA_ATOM, 32, PropModeReplace, (unsigned char *)&r->wm_delete, 1); XMapWindow(r->sk.dpy, r->win); XFlush(r->sk.dpy); r->running = 1; } /* ── Event loop ──────────────────────────────────────────────────── */ static void run(Rigel *r) { draw(r); while (r->running) { XEvent ev; XNextEvent(r->sk.dpy, &ev); /* ── context menu events ─────────────────────────────── */ if (r->ctx_vis && ev.xany.window == r->ctx_win) { if (ev.type == Expose) { ctx_draw(r); } else if (ev.type == MotionNotify) { int it = ctx_item_at(ev.xmotion.y); if (it != r->ctx_hover) { r->ctx_hover = it; ctx_draw(r); } } else if (ev.type == ButtonPress) { int it = ctx_item_at(ev.xbutton.y); ctx_hide(r); if (it >= 0 && !CTX[it].sep) ctx_exec(r, it); draw(r); } else if (ev.type == LeaveNotify) { r->ctx_hover = -1; ctx_draw(r); } continue; } /* click outside context menu */ if (r->ctx_vis && (ev.type == ButtonPress || ev.type == KeyPress)) { ctx_hide(r); draw(r); if (ev.type == KeyPress) on_key(r, &ev.xkey); continue; } /* ── main window events ──────────────────────────────── */ switch (ev.type) { case Expose: if (ev.xexpose.count == 0) draw(r); break; case ButtonPress: if (ev.xbutton.button == Button1 || ev.xbutton.button == Button3) on_click(r, ev.xbutton.x, ev.xbutton.y, ev.xbutton.time, ev.xbutton.button); else if (ev.xbutton.button == Button4) on_scroll(r, -1); else if (ev.xbutton.button == Button5) on_scroll(r, 1); break; case MotionNotify: on_motion(r, ev.xmotion.x, ev.xmotion.y); break; case LeaveNotify: if (r->hover_row >= 0 || r->hover_btn != 0 || r->sidebar_hover >= 0) { r->hover_row = -1; r->hover_btn = 0; r->sidebar_hover = -1; draw(r); } break; case KeyPress: on_key(r, &ev.xkey); break; case ClientMessage: if ((Atom)ev.xclient.data.l[0] == r->wm_delete) r->running = 0; break; case DestroyNotify: r->running = 0; break; } } } int main(void) { Rigel r = {0}; init(&r); run(&r); return 0; }