From b3b86808eac803d57e3ea9ce0f83e13f2f8e02e0 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 1 Feb 2024 00:49:46 -0500 Subject: [PATCH] XmPackage stub --- xmpackage/build.sh | 2 + xmpackage/src/icons/icon.xbm | 27 ++ xmpackage/src/main.c | 640 +++++++++++++++++++++++++++++++++++ 3 files changed, 669 insertions(+) create mode 100755 xmpackage/build.sh create mode 100644 xmpackage/src/icons/icon.xbm create mode 100644 xmpackage/src/main.c diff --git a/xmpackage/build.sh b/xmpackage/build.sh new file mode 100755 index 0000000..4aeec25 --- /dev/null +++ b/xmpackage/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +../scripts/buildapp.sh xmpackage "$@" diff --git a/xmpackage/src/icons/icon.xbm b/xmpackage/src/icons/icon.xbm new file mode 100644 index 0000000..af617e5 --- /dev/null +++ b/xmpackage/src/icons/icon.xbm @@ -0,0 +1,27 @@ +#define icon_width 48 +#define icon_height 48 +static unsigned char icon_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, + 0x00, 0x00, 0x78, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x78, 0x00, 0x00, + 0x00, 0x80, 0x07, 0xe0, 0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x0e, 0x00, + 0x00, 0x78, 0x00, 0x40, 0x38, 0x00, 0x00, 0x1e, 0x80, 0x31, 0x60, 0x00, + 0x80, 0x07, 0x40, 0x0e, 0x80, 0x01, 0xc0, 0x01, 0x20, 0x04, 0x00, 0x03, + 0xe0, 0x00, 0xc0, 0x08, 0x00, 0x07, 0x60, 0x03, 0x30, 0x07, 0x80, 0x06, + 0x60, 0x0c, 0x0c, 0x00, 0x20, 0x06, 0x60, 0x30, 0x03, 0x00, 0x08, 0x06, + 0x60, 0xc0, 0x06, 0x00, 0x02, 0x06, 0x60, 0x00, 0x0b, 0x80, 0x60, 0x06, + 0x60, 0x00, 0x3c, 0x20, 0x58, 0x06, 0x60, 0x00, 0xf0, 0x08, 0x46, 0x06, + 0x60, 0x00, 0xc0, 0x83, 0x41, 0x06, 0x60, 0x00, 0x00, 0x81, 0x60, 0x06, + 0x60, 0x00, 0x00, 0x80, 0x18, 0x06, 0x60, 0x00, 0x00, 0x81, 0x06, 0x06, + 0x60, 0x00, 0x00, 0x80, 0x01, 0x06, 0x60, 0x00, 0x00, 0x01, 0x00, 0x06, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x01, 0x00, 0x06, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0x01, 0x02, 0x06, + 0x60, 0x00, 0x00, 0x80, 0x02, 0x06, 0x60, 0x00, 0x00, 0xa1, 0x02, 0x06, + 0xc0, 0x01, 0x00, 0xa8, 0x82, 0x03, 0x80, 0x07, 0x00, 0xa9, 0xe0, 0x01, + 0x00, 0x1e, 0x00, 0x28, 0x78, 0x00, 0x00, 0x78, 0x00, 0x09, 0x1e, 0x00, + 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0x80, 0x07, 0xe1, 0x01, 0x00, + 0x00, 0x00, 0x1e, 0x78, 0x00, 0x00, 0x00, 0x00, 0x78, 0x1f, 0x00, 0x00, + 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; diff --git a/xmpackage/src/main.c b/xmpackage/src/main.c new file mode 100644 index 0000000..fb12e6a --- /dev/null +++ b/xmpackage/src/main.c @@ -0,0 +1,640 @@ +#define _XOPEN_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "icons/icon.xbm" + +Widget createLabeledTextForm (Widget, String, String, XmString); +Widget createScrolledInfoText (Widget, String); +void selectPackage (String); +void updatePackageList (void); +void readPackageList (FILE *); +void handleListModeSelectionChange (Widget, XtPointer, XtPointer); +void handlePackageSelectionChange (Widget, XtPointer, XtPointer); +void handleSearchQueryActivate (Widget, XtPointer, XtPointer); +String parsePackageName (String); +void parseInfoOutput (FILE *, String *, String *, String *, String *); + +static XtAppContext application; +static Pixmap iconPixmap; + +static struct { + Widget name; + Widget description; + Widget dependencies; + Widget provides; + Widget contents; + String selected; +} packageInfo = { 0 }; + +static struct { + Widget query; + Widget list; + Widget modeMenu; + enum { + viewModeWorld, + viewModeInstalled, + viewModeUpgradable, + viewModeOrphaned, + viewModeSearch + } viewMode; + XmString viewModeStrings[4]; +} packageList = { 0 }; + +int main (int argc, char *argv[]) { + Widget topLevel = XtVaAppInitialize ( + &application, "PackageManager", + NULL, 0, + &argc, argv, + NULL, + XmNtitle, "Package Manager", + XmNiconName, "Package Manager", + NULL); + iconPixmap = XmdLoadBitmapIcon(topLevel, icon); + XtVaSetValues ( + topLevel, + XmNiconPixmap, iconPixmap, + NULL); + Widget window = XtVaCreateManagedWidget ( + "window", xmMainWindowWidgetClass, topLevel, + NULL); + Widget pane = XtVaCreateWidget ( + "pane", xmPanedWindowWidgetClass, window, + XmNorientation, XmHORIZONTAL, + NULL); + + /* menu bar */ + XmString fileString = XmStringCreateLocalized("File"); + XmString systemString = XmStringCreateLocalized("System"); + XmString helpString = XmStringCreateLocalized("Help"); + Widget menuBar = XmVaCreateSimpleMenuBar ( + window, "menuBar", + XmVaCASCADEBUTTON, fileString, 'F', + XmVaCASCADEBUTTON, systemString, 'S', + XmVaCASCADEBUTTON, helpString, 'H', + NULL); + XmStringFree(fileString); + XmStringFree(systemString); + XmStringFree(helpString); + XtManageChild(menuBar); + + /* package list pane */ + Widget packageListPane = XtVaCreateWidget ( + "packageListPane", xmFormWidgetClass, pane, + XmNhorizontalSpacing, 4, + XmNverticalSpacing, 4, + XmNmarginHeight, 2, + XmNmarginWidth, 2, + NULL); + XmString packageSearchLabelString = XmStringCreateLocalized("Search:"); + packageList.query = createLabeledTextForm ( + packageListPane, + "packageSearchLabel", + "packageSearchText", + packageSearchLabelString); + XmStringFree(packageSearchLabelString); + XtAddCallback ( + packageList.query, + XmNactivateCallback, handleSearchQueryActivate, + NULL); + XtVaSetValues ( + XtParent(packageList.query), + XmNrightAttachment, XmATTACH_FORM, + XmNtopAttachment, XmATTACH_FORM, + XmNleftAttachment, XmATTACH_FORM, + NULL); + + /* list mode selector */ + Widget listModeFrame = XtVaCreateWidget ( + "listModeFrame", xmFrameWidgetClass, packageListPane, + XmNshadowType, XmSHADOW_IN, + XmNshadowThickness, 1, + XmNleftAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNbottomAttachment, XmATTACH_FORM, + NULL); + Widget listModeRc = XtVaCreateWidget ( + "listModeRc", xmRowColumnWidgetClass, listModeFrame, + XmNmarginWidth, 0, + XmNmarginHeight, 0, + NULL); + XmString listModeString = XmStringCreateLocalized("List:"); + packageList.viewModeStrings[viewModeWorld] = XmStringCreateLocalized("World"); + packageList.viewModeStrings[viewModeInstalled] = XmStringCreateLocalized("Installed"); + packageList.viewModeStrings[viewModeUpgradable] = XmStringCreateLocalized("Upgradable"); + packageList.viewModeStrings[viewModeOrphaned] = XmStringCreateLocalized("Orphaned"); + packageList.viewModeStrings[viewModeSearch] = XmStringCreateLocalized("Search"); + packageList.modeMenu = XmVaCreateSimpleOptionMenu ( + listModeRc, "packageListMode", + listModeString, 'L', 0, handleListModeSelectionChange, + XmVaPUSHBUTTON, packageList.viewModeStrings[viewModeWorld], 'W', NULL, NULL, + XmVaPUSHBUTTON, packageList.viewModeStrings[viewModeInstalled], 'I', NULL, NULL, + XmVaPUSHBUTTON, packageList.viewModeStrings[viewModeUpgradable], 'U', NULL, NULL, + XmVaPUSHBUTTON, packageList.viewModeStrings[viewModeOrphaned], 'O', NULL, NULL, + XmVaPUSHBUTTON, packageList.viewModeStrings[viewModeSearch], 'S', NULL, NULL, + XmNtearOffModel, XmTEAR_OFF_ENABLED, + NULL); + XmStringFree(listModeString); + XtManageChild(packageList.modeMenu); + XtManageChild(listModeFrame); + XtManageChild(listModeRc); + + /* packageList */ + packageList.list = XmCreateScrolledList(packageListPane, "packageList", NULL, 0); + XtVaSetValues ( + XtParent(packageList.list), + XmNtopAttachment, XmATTACH_WIDGET, + XmNtopWidget, XtParent(packageList.query), + XmNleftAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNbottomAttachment, XmATTACH_WIDGET, + XmNbottomWidget, listModeFrame, + NULL); + XtAddCallback ( + packageList.list, + XmNbrowseSelectionCallback, handlePackageSelectionChange, + NULL); + XtManageChild(packageList.list); + + /* package viewer */ + Widget descriptionPane = XtVaCreateWidget ( + "descriptionPane", xmFormWidgetClass, pane, + XmNhorizontalSpacing, 4, + XmNverticalSpacing, 4, + XmNmarginHeight, 2, + XmNmarginWidth, 2, + NULL); + + /* package actions */ + Widget packageActionsFrame = XtVaCreateWidget ( + "packageActionsFrame", xmFrameWidgetClass, descriptionPane, + XmNshadowType, XmSHADOW_OUT, + XmNleftAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNbottomAttachment, XmATTACH_FORM, + NULL); + Widget packageActionsRc = XtVaCreateWidget ( + "packageActionsRc", xmRowColumnWidgetClass, packageActionsFrame, + XmNorientation, XmHORIZONTAL, + NULL); + XmString packageAddString = XmStringCreateLocalized("Install"); + Widget packageAdd = XtVaCreateManagedWidget ( + "packageAdd", xmPushButtonWidgetClass, packageActionsRc, + XmNlabelString, packageAddString, + NULL); + XmStringFree(packageAddString); + XmString packageDeleteString = XmStringCreateLocalized("Remove"); + XtVaCreateManagedWidget ( + "packageDelete", xmPushButtonWidgetClass, packageActionsRc, + XmNlabelString, packageDeleteString, + NULL); + XmStringFree(packageDeleteString); + XmString packageUpgradeString = XmStringCreateLocalized("Upgrade"); + XtVaCreateManagedWidget ( + "packageUpgrade", xmPushButtonWidgetClass, packageActionsRc, + XmNlabelString, packageUpgradeString, + NULL); + XmStringFree(packageUpgradeString); + XmString packageFixString = XmStringCreateLocalized("Fix"); + XtVaCreateManagedWidget ( + "packageFix", xmPushButtonWidgetClass, packageActionsRc, + XmNlabelString, packageFixString, + NULL); + XmStringFree(packageFixString); + XtManageChild(packageActionsRc); + XtManageChild(packageActionsFrame); + + /* visually separate package description and actions */ + Widget descriptionSeparator = XtVaCreateManagedWidget ( + "descriptionSeparator", xmSeparatorWidgetClass, descriptionPane, + XmNleftAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNbottomAttachment, XmATTACH_WIDGET, + XmNbottomWidget, packageAdd, + NULL); + + /* package description */ + packageInfo.name = XtVaCreateManagedWidget ( + "name", xmLabelGadgetClass, descriptionPane, + XmNleftAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNtopAttachment, XmATTACH_FORM, + NULL); + Widget descriptionTabStack = XtVaCreateWidget ( + "descriptionTabStack", xmTabStackWidgetClass, descriptionPane, + XmNleftAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNtopAttachment, XmATTACH_WIDGET, + XmNtopWidget, packageInfo.name, + XmNbottomAttachment, XmATTACH_WIDGET, + XmNbottomWidget, descriptionSeparator, + XmNtabMarginWidth, 0, + XmNtabMarginHeight, 0, + XmNtabLabelSpacing, 0, + XmNmarginWidth, 4, + XmNmarginHeight, 4, + XmNtabMode, XmTABS_STACKED, + NULL); + packageInfo.description = createScrolledInfoText(descriptionTabStack, "Description"); + packageInfo.dependencies = createScrolledInfoText(descriptionTabStack, "Dependencies"); + packageInfo.provides = createScrolledInfoText(descriptionTabStack, "Provides"); + packageInfo.contents = createScrolledInfoText(descriptionTabStack, "Contents"); + XtManageChild(descriptionTabStack); + + /* final setup */ + + XtManageChild(packageListPane); + XtManageChild(descriptionPane); + XtManageChild(pane); + + XtVaSetValues ( + window, + XmNmenuBar, menuBar, + XmNworkWindow, pane, + NULL); + + updatePackageList(); + selectPackage(NULL); + + XtRealizeWidget(topLevel); + XtAppMainLoop(application); +} + +Widget createLabeledTextForm ( + Widget parent, + String labelName, + String textName, + XmString labelString +) { + Widget form = XtVaCreateWidget ( + "form", xmFormWidgetClass, parent, + XmNorientation, XmHORIZONTAL, + NULL); + Widget label = XtVaCreateManagedWidget ( + labelName, xmLabelGadgetClass, form, + XmNleftAttachment, XmATTACH_FORM, + XmNtopAttachment, XmATTACH_FORM, + XmNbottomAttachment, XmATTACH_FORM, + XmNlabelString, labelString, + NULL); + Widget text = XtVaCreateManagedWidget ( + textName, xmTextWidgetClass, form, + XmNleftAttachment, XmATTACH_WIDGET, + XmNleftWidget, label, + XmNtopAttachment, XmATTACH_FORM, + XmNrightAttachment, XmATTACH_FORM, + XmNbottomAttachment, XmATTACH_FORM, + NULL); + XtManageChild (form); + return text; +} + +Widget createScrolledInfoText (Widget parent, String name) { + Widget scroll = XtVaCreateWidget ( + name, xmScrolledWindowWidgetClass, parent, + NULL); + Widget info = XtVaCreateManagedWidget ( + "info", xmTextWidgetClass, scroll, + XmNrows, 10, + XmNcolumns, 60, + XmNpaneMinimum, 35, + XmNeditMode, XmMULTI_LINE_EDIT, + XmNeditable, False, + XmNwordWrap, True, + XmNscrollHorizontal, False, + NULL); + XtManageChild(scroll); + return info; +} + +void selectPackage (String name) { + if (packageInfo.selected) XtFree(packageInfo.selected); + packageInfo.selected = name; + + XmString packageNameString = NULL; + + if (name == NULL) { + packageNameString = XmStringCreateLocalized("No package selected"); + XtVaSetValues ( + packageInfo.description, + XmNvalue, "", + NULL); + } else { + /* instruct apk to describe package */ + pid_t child; + FILE *stream = XmdVaPipedExecPath ( + "apk", &child, "r", + "apk", "info", name, "-dswLPR", "--license", + NULL); + if (stream == NULL) { + /* TODO: error popup */ + return; + } + + /* parse data*/ + String description = NULL; + String dependencies = NULL; + String provides = NULL; + String contents = NULL; + parseInfoOutput ( + stream, + &description, &dependencies, + &provides, &contents); + fclose(stream); + waitpid(child, NULL, 0); + + /* set tab contents to parsed data */ + packageNameString = XmStringCreateLocalized(name); + XtVaSetValues ( + packageInfo.description, + XmNvalue, description, + NULL); + XtVaSetValues ( + packageInfo.dependencies, + XmNvalue, dependencies, + NULL); + XtVaSetValues ( + packageInfo.provides, + XmNvalue, provides, + NULL); + XtVaSetValues ( + packageInfo.contents, + XmNvalue, contents, + NULL); + XtFree(description); + XtFree(dependencies); + XtFree(provides); + XtFree(contents); + } + + XtVaSetValues ( + packageInfo.name, + XmNlabelString, packageNameString, + NULL); + XmStringFree(packageNameString); +} + +void parseInfoOutput ( + FILE *stream, + String *description, + String *dependencies, + String *provides, + String *contents +) { + char nullTerminator = 0; + + enum { + stateSkipUntilSpace, + stateGetHeading, + stateGetSection + } state = stateSkipUntilSpace; + int consecutiveNewlines = 0; + XmdBuffer *activeBuffer = NULL; + XmdBuffer *headingBuffer = NULL; + + XmdBuffer *descriptionBuffer = XmdBufferNew(char); + XmdBuffer *dependenciesBuffer = XmdBufferNew(char); + XmdBuffer *providesBuffer = XmdBufferNew(char); + XmdBuffer *contentsBuffer = XmdBufferNew(char); + + int ch; + while ((ch = fgetc(stream)) != EOF) switch (state) { + /* discard stream until there is a space */ + case stateSkipUntilSpace: + if (ch == ' ') { + headingBuffer = XmdBufferNew(char); + state = stateGetHeading; + } + break; + + /* what is the next section about? */ + case stateGetHeading: + if (ch == ':') { + XmdBufferPush(headingBuffer, &nullTerminator); + String heading = XmdBufferBreak(headingBuffer); + + if (strcmp(heading, "depends on") == 0) { + activeBuffer = dependenciesBuffer; + } else if (strcmp(heading, "provides") == 0) { + activeBuffer = providesBuffer; + } else if (strcmp(heading, "contains") == 0) { + activeBuffer = contentsBuffer; + } else { + activeBuffer = descriptionBuffer; + } + + XtFree(heading); + consecutiveNewlines = 1; + state = stateGetSection; + fgetc(stream); /* skip newline */ + } else { + XmdBufferPush(headingBuffer, &ch); + } + break; + + /* read the section data into the active buffer */ + case stateGetSection: + if (ch == '\n') { + if (consecutiveNewlines > 0) { + state = stateSkipUntilSpace; + break; + } + consecutiveNewlines ++; + } else { + consecutiveNewlines = 0; + } + XmdBufferPush(activeBuffer, &ch); + break; + } + + XmdBufferPush(descriptionBuffer, &nullTerminator); + XmdBufferPush(dependenciesBuffer, &nullTerminator); + XmdBufferPush(providesBuffer, &nullTerminator); + XmdBufferPush(contentsBuffer, &nullTerminator); + + *description = XmdBufferBreak(descriptionBuffer); + *dependencies = XmdBufferBreak(dependenciesBuffer); + *provides = XmdBufferBreak(providesBuffer); + *contents = XmdBufferBreak(contentsBuffer); +} + +void updatePackageList (void) { + FILE *stream = NULL; + pid_t child = 0; + + switch (packageList.viewMode) { + case viewModeWorld: + stream = fopen("/etc/apk/world", "r"); + break; + case viewModeInstalled: + stream = XmdVaPipedExecPath ( + "apk", &child, "r", + "apk", "list", "--installed", + NULL); + break; + case viewModeUpgradable: + stream = XmdVaPipedExecPath ( + "apk", &child, "r", + "apk", "list", "--upgradable", + NULL); + break; + case viewModeOrphaned: + stream = XmdVaPipedExecPath ( + "apk", &child, "r", + "apk", "list", "--orphaned", + NULL); + break; + case viewModeSearch: + ;String query = NULL; + XtVaGetValues ( + packageList.query, + XmNvalue, &query, + NULL); + stream = XmdVaPipedExecPath ( + "apk", &child, "r", + "apk", "search", query, + NULL); + XtFree(query); + break; + } + + if (stream == NULL) { + /* TODO: error popup */ + return; + } + + readPackageList(stream); + fclose(stream); + if (child != 0) waitpid(child, NULL, 0); +} + +void readPackageList (FILE *list) { + String line = NULL; + size_t lineLength = 0; + + char packageName[256] = { 0 }; + + XmdBuffer *itemsBuffer = XmdBufferNew(XmString); + + while (getline(&line, &lineLength, list) != -1) { + sscanf(line, "%256s", packageName); + free(line); + line = NULL; + XmString packageNameString = XmStringCreateLocalized(packageName); + XmdBufferPush(itemsBuffer, &packageNameString); + } + + Cardinal length = XmdBufferLength(itemsBuffer); + XmStringTable items = (XmStringTable)(XmdBufferBreak(itemsBuffer)); + + XtVaSetValues ( + packageList.list, + XmNitemCount, length, + XmNitems, items, + NULL); + + /* free everything */ + if (line) free(line); + for (size_t index = 0; index < length; index ++) { + XmStringFree(items[index]); + } + XtFree((String)(items)); +} + +void handleListModeSelectionChange ( + Widget widget, + XtPointer clientData, + XtPointer callData +) { + (void)(widget); + (void)(callData); + + int selected = *(int *)(&clientData); + packageList.viewMode = selected; + updatePackageList(); +} + +void handlePackageSelectionChange ( + Widget widget, + XtPointer clientData, + XtPointer callData +) { + (void)(widget); + (void)(clientData); + + XmListCallbackStruct *event = (XmListCallbackStruct*)(callData); + if (event->event == NULL) return; + + String choice = (String)(XmStringUnparse ( + event->item, + XmFONTLIST_DEFAULT_TAG, + XmCHARSET_TEXT, + XmCHARSET_TEXT, + NULL, 0, + XmOUTPUT_ALL)); + selectPackage(parsePackageName(choice)); + XtFree(choice); +} + +void handleSearchQueryActivate ( + Widget widget, + XtPointer clientData, + XtPointer callData +) { + (void)(widget); + (void)(clientData); + (void)(callData); + + packageList.viewMode = viewModeSearch; + + /* FIXME do this by activating the appropriate button. that way we dont + have to call updatePackageList manually as well. */ + Widget button = XmOptionButtonGadget(packageList.modeMenu); + XtVaSetValues ( + button, + XmNlabelString, packageList.viewModeStrings[packageList.viewMode], + NULL); + + updatePackageList(); +} + +String parsePackageName (String input) { + XmdBuffer *output = XmdBufferNew(char); + char ch; + Boolean wasDash = False; + for (size_t index = 0; (ch = input[index]); index ++) { + if (wasDash && isdigit(ch)) { + XmdBufferPop(output, NULL); + break; + } else { + wasDash = ch == '-'; + XmdBufferPush(output, &ch); + } + } + char nullTerminator = 0; + XmdBufferPush(output, &nullTerminator); + return XmdBufferBreak(output); +}