diff --git a/art/themes/luciano_blocktronics/MMENU.ANS b/art/themes/luciano_blocktronics/MMENU.ANS index 28792570..cf395ec0 100644 Binary files a/art/themes/luciano_blocktronics/MMENU.ANS and b/art/themes/luciano_blocktronics/MMENU.ANS differ diff --git a/art/themes/luciano_blocktronics/activitypub_menu.ans b/art/themes/luciano_blocktronics/activitypub_menu.ans new file mode 100644 index 00000000..5530a12d Binary files /dev/null and b/art/themes/luciano_blocktronics/activitypub_menu.ans differ diff --git a/art/themes/luciano_blocktronics/activitypub_social_manager.ans b/art/themes/luciano_blocktronics/activitypub_social_manager.ans index 5348f1b2..109fed08 100644 Binary files a/art/themes/luciano_blocktronics/activitypub_social_manager.ans and b/art/themes/luciano_blocktronics/activitypub_social_manager.ans differ diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 2b4588bc..77497d37 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -498,6 +498,9 @@ width: 35 itemFormat: "|00|03{subject}|00 {statusIndicator}" focusItemFormat: "|00|19|15{subject!styleUpper}|00 {statusIndicator}" + itemFormat: "|00{statusIndicator} |00|03{subject}" + focusItemFormat: "|00{statusIndicator} |00|19|15{subject}" + textOverflow: "..." } MT2: { height: 15 diff --git a/core/activitypub/actor_search.js b/core/activitypub/actor_search.js index 36045e45..cc1f2e6a 100644 --- a/core/activitypub/actor_search.js +++ b/core/activitypub/actor_search.js @@ -303,6 +303,9 @@ exports.getModule = class ActivityPubActorSearch extends MenuModule { async.series( [ callback => { + if (this.viewControllers.view) { + this.viewControllers.view.setFocus(false); + } return this.displayArtAndPrepViewController( 'main', FormIds.main, diff --git a/core/full_menu_view.js b/core/full_menu_view.js index b3bb2415..7b20b8f5 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,7 +59,7 @@ function FullMenuView(options) { } for (let i = 0; i < this.dimens.height; i++) { - const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + const text = strUtil.pad('', width, this.fillChar); this.client.term.write( `${ansi.goto( this.position.row + i, @@ -77,6 +77,7 @@ function FullMenuView(options) { this.autoAdjustHeightIfEnabled(); this.pages = []; // reset + this.currentPage = 0; // reset currentPage when pages reset // Calculate number of items visible per column this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); @@ -240,14 +241,25 @@ function FullMenuView(options) { sgr = index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR(); } - let renderLength = strUtil.renderStringLength(text); - if (this.hasTextOverflow() && item.col + renderLength > this.dimens.width) { - text = - strUtil.renderSubstr( - text, - 0, - this.dimens.width - (item.col + this.textOverflow.length) - ) + this.textOverflow; + const renderLength = strUtil.renderStringLength(text); + + let relativeColumn = item.col - this.position.col; + if (relativeColumn < 0) { + relativeColumn = 0; + this.client.log.warn( + { itemCol: item.col, positionColumn: this.position.col }, + 'Invalid item column detected in full menu' + ); + } + + if (relativeColumn + renderLength > this.dimens.width) { + if (this.hasTextOverflow()) { + text = strUtil.renderTruncate(text, { + length: + this.dimens.width - (relativeColumn + this.textOverflow.length), + omission: this.textOverflow, + }); + } } let padLength = Math.min(item.fixedLength + 1, this.dimens.width); @@ -270,14 +282,29 @@ FullMenuView.prototype.redraw = function () { this.cachePositions(); + // In case we get in a bad state, try to recover + if (this.currentPage < 0) { + this.currentPage = 0; + } + if (this.items.length) { - for ( - let i = this.pages[this.currentPage].start; - i <= this.pages[this.currentPage].end; - ++i + if ( + this.currentPage > this.pages.length || + !_.isObject(this.pages[this.currentPage]) ) { - this.items[i].focused = this.focusedItemIndex === i; - this.drawItem(i); + this.client.log.warn( + { currentPage: this.currentPage, pagesLength: this.pages.length }, + 'Invalid state! in full menu redraw' + ); + } else { + for ( + let i = this.pages[this.currentPage].start; + i <= this.pages[this.currentPage].end; + ++i + ) { + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } } } }; @@ -358,6 +385,10 @@ FullMenuView.prototype.setItems = function (items) { this.oldDimens = Object.assign({}, this.dimens); } + // Reset the page on new items + this.currentPage = 0; + this.focusedItemIndex = 0; + FullMenuView.super_.prototype.setItems.call(this, items); this.positionCacheExpired = true; @@ -378,10 +409,20 @@ FullMenuView.prototype.focusNext = function () { this.focusedItemIndex = 0; this.currentPage = 0; } else { - this.focusedItemIndex++; - if (this.focusedItemIndex > this.pages[this.currentPage].end) { - this.clearPage(); - this.currentPage++; + if ( + this.currentPage > this.pages.length || + !_.isObject(this.pages[this.currentPage]) + ) { + this.client.log.warn( + { currentPage: this.currentPage, pagesLength: this.pages.length }, + 'Invalid state in focusNext for full menu view' + ); + } else { + this.focusedItemIndex++; + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } } } @@ -397,9 +438,19 @@ FullMenuView.prototype.focusPrevious = function () { this.currentPage = this.pages.length - 1; } else { this.focusedItemIndex--; - if (this.focusedItemIndex < this.pages[this.currentPage].start) { - this.clearPage(); - this.currentPage--; + if ( + this.currentPage > this.pages.length || + !_.isObject(this.pages[this.currentPage]) + ) { + this.client.log.warn( + { currentPage: this.currentPage, pagesLength: this.pages.length }, + 'Bad focus state, ignoring call to focusPrevious.' + ); + } else { + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); + this.currentPage--; + } } } diff --git a/core/oputil/oputil_config.js b/core/oputil/oputil_config.js index d979f9b7..0bde1a08 100644 --- a/core/oputil/oputil_config.js +++ b/core/oputil/oputil_config.js @@ -250,6 +250,7 @@ function buildNewConfig() { 'new_user.in.hjson', 'doors.in.hjson', 'file_base.in.hjson', + 'activitypub.in.hjson', ]; let includeFiles = []; diff --git a/core/string_util.js b/core/string_util.js index 6f1a6a01..2ff418ea 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -145,29 +145,29 @@ function pad(s, len, padChar, justify, stringSGR, padSGR, useRenderLen = true) { padSGR = padSGR || ''; const renderLen = useRenderLen ? renderStringLength(s) : s.length; - const padlen = len >= renderLen ? len - renderLen : 0; + const padLen = len > renderLen ? len - renderLen : 0; switch (justify) { case 'L': case 'left': - s = `${stringSGR}${s}${padSGR}${Array(padlen).join(padChar)}`; + s = `${stringSGR}${s}${padSGR}${padChar.repeat(padLen)}`; break; case 'C': case 'center': case 'both': { - const right = Math.ceil(padlen / 2); - const left = padlen - right; + const right = Math.ceil(padLen / 2); + const left = padLen - right; s = `${padSGR}${Array(left + 1).join( padChar - )}${stringSGR}${s}${padSGR}${Array(right + 1).join(padChar)}`; + )}${stringSGR}${s}${padSGR}${padChar.repeat(right)}`; } break; case 'R': case 'right': - s = `${padSGR}${Array(padlen).join(padChar)}${stringSGR}${s}`; + s = `${padSGR}${padChar.repeat(padLen)}${stringSGR}${s}`; break; default: diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index d8c2875a..f08a66e4 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -98,10 +98,12 @@ function VerticalMenuView(options) { sgr = index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR(); } - text = strUtil.renderTruncate(text, { - length: this.dimens.width, - omission: this.truncateOmission, - }); + if (this.hasTextOverflow()) { + text = strUtil.renderTruncate(text, { + length: this.dimens.width, + omission: this.textOverflow, + }); + } text = `${sgr}${strUtil.pad( `${text}${this.styleSGR1}`, @@ -143,18 +145,16 @@ VerticalMenuView.prototype.redraw = function () { // erase old items // :TODO: optimize this: only needed if a item is removed or new max width < old. if (this.oldDimens) { - const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join( - ' ' - ); - let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; - let row = this.position.row + 1; + const blank = ' '.repeat(Math.max(this.oldDimens.width, this.dimens.width)); + let row = this.position.row; const endRow = row + this.oldDimens.height - 2; while (row <= endRow) { - seq += ansi.goto(row, this.position.col) + blank; + this.client.term.write( + ansi.goto(row, this.position.col) + this.getSGR() + blank + ); row += 1; } - this.client.term.write(seq); delete this.oldDimens; } @@ -247,6 +247,7 @@ VerticalMenuView.prototype.setItems = function (items) { if (this.items && this.items.length) { this.oldDimens = Object.assign({}, this.dimens); } + this.focusedItemIndex = 0; VerticalMenuView.super_.prototype.setItems.call(this, items); @@ -406,6 +407,12 @@ VerticalMenuView.prototype.focusLast = function () { return VerticalMenuView.super_.prototype.focusLast.call(this); }; +VerticalMenuView.prototype.setTextOverflow = function (overflow) { + VerticalMenuView.super_.prototype.setTextOverflow.call(this, overflow); + + this.positionCacheExpired = true; +}; + VerticalMenuView.prototype.setFocusItems = function (items) { VerticalMenuView.super_.prototype.setFocusItems.call(this, items); diff --git a/core/view.js b/core/view.js index a82751b5..ed8f08d0 100644 --- a/core/view.js +++ b/core/view.js @@ -50,7 +50,6 @@ function View(options) { this.textStyle = options.textStyle || 'normal'; this.focusTextStyle = options.focusTextStyle || this.textStyle; this.offsetsApplied = false; - this.truncateOmission = options.truncateOmission || ''; if (options.id) { this.setId(options.id); @@ -274,12 +273,6 @@ View.prototype.setPropertyValue = function (propName, value) { this.validate = value; } break; - - case 'truncateOmission': - if (_.isString(value)) { - this.truncateOmission = value; - } - break; } if (/styleSGR[0-9]{1,2}/.test(propName)) { diff --git a/docs/_docs/art/views/vertical_menu_view.md b/docs/_docs/art/views/vertical_menu_view.md index e11e087c..4a6690b8 100644 --- a/docs/_docs/art/views/vertical_menu_view.md +++ b/docs/_docs/art/views/vertical_menu_view.md @@ -29,9 +29,9 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a | `justify` | Sets the justification of each item in the list. Options: left (default), right, center | | `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) | | `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `textOverflow` | If an entry cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below | | `items` | List of items to show in the menu. See **Items** below. | `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | -| `truncateOmission` | Sets the omission characters for truncated text if used. Defaults to an empty string. Commonly set to "..." | ### Hot Keys @@ -70,6 +70,15 @@ If the list is for display only (there is no form action associated with it) you ["First item", "Second item", "Third Item"] ``` +### Text Overflow + +The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. If an entry is too long to display in the width specified + +> :information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column. + +> :information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +> :information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` ## Example diff --git a/misc/menu_templates/activitypub.in.hjson b/misc/menu_templates/activitypub.in.hjson new file mode 100644 index 00000000..8621371d --- /dev/null +++ b/misc/menu_templates/activitypub.in.hjson @@ -0,0 +1,264 @@ +{ + "menus": { + "activityPubMenu": { + "desc": "Menu for all ActivityPub actions", + "art": "activitypub_menu", + "prompt": "menuCommand", + "submit": [ + { + "value": { + "command": "S" + }, + "action": "@menu:activityPubActorSearch" + }, + { + "value": { + "command": "C" + }, + "action": "@menu:activityPubUserConfig" + }, + { + "value": { + "command": "M" + }, + "action": "@menu:activityPubFollowingManager" + }, + { + "value": { + "command": "Q" + }, + "action": "@menu:mainMenu" + }, + ] + }, + "activityPubActorSearch": { + "desc": "Viewing ActivityPub", + "module": "activitypub/actor_search", + "config": { + "cls": true, + "art": { + "main": "activitypub_actor_search_main", + "view": "activitypub_actor_view" + } + }, + "form": { + "0": { + "mci": { + "ET1": { + "focus": true, + "maxLength": 70, + "argName": "searchQuery", + "submit": true + } + }, + "submit": { + "*": [ + { + "value": { + "searchQuery": null + }, + "action": "@method:search" + } + ] + }, + "actionKeys": [ + { + "keys": [ + "escape" + ], + "action": "@systemMethod:prevMenu" + } + ] + }, + "1": { + "mci": {}, + "actionKeys": [ + { + "keys": [ + "escape", + "q", + "shift + q" + ], + "action": "@method:backKeyPressed" + }, + { + "keys": [ + "space" + ], + "action": "@method:toggleFollowKeyPressed" + } + ] + } + } + }, + "activityPubUserConfig": { + "desc": "ActivityPub Config", + "module": "./activitypub/user_config", + "config": { + "art": { + "main": "activitypub_user_config_main", + "images": "activitypub_user_config_images" + } + }, + "form": { + "0": { + "mci": { + "TM1": { + "focus": true, + "items": [ + "yes", + "no" + ], + "argName": "enabled" + }, + "TM2": { + "items": [ + "yes", + "no" + ], + "argName": "manuallyApproveFollowers" + }, + "TM3": { + "items": [ + "yes", + "no" + ], + "argName": "hideSocialGraph" + }, + "TM4": { + "items": [ + "yes", + "no" + ], + "argName": "showRealName" + }, + "TL5": { + "argName": "image" + }, + "TL6": { + "argName": "icon" + }, + "BT7": { + "text": "manage images", + "argName": "manageImages", + "submit": true, + "justify": "center" + }, + "TM8": { + "items": [ + "save", + "cancel" + ], + "submit": true, + "argName": "saveOrCancel" + } + }, + "submit": { + "*": [ + { + // :TODO: we need a way to just want the argName *for the submitting item* and drop others + "value": { + "manageImages": null + }, + "action": "@method:mainSubmit" + } + ] + }, + "actionKeys": "@reference:common.escToPrev" + }, + "1": { + "mci": { + "ML1": { + "argName": "imageUrl", + "mode": "edit", + "scrollMode": "start", + "focus": true, + "tabSwitchesView": true + }, + "ML2": { + "argName": "iconUrl", + "mode": "edit", + "scrollMode": "start", + "tabSwitchesView": true + }, + "TM3": { + "items": [ + "save", + "cancel" + ], + "submit": true, + "argName": "imagesSaveOrCancel" + } + }, + "submit": { + "*": [ + { + "value": { + "imagesSaveOrCancel": null + }, + "action": "@method:imagesSubmit" + } + ] + }, + "actionKeys": [ + { + "keys": [ + "escape" + ], + "action": "@method:backToMain" + } + ] + } + } + }, + "activityPubFollowingManager": { + "desc": "Social Manager", + "module": "./activitypub/social_manager", + "config": { + "art": { + "main": "activitypub_social_manager" + } + }, + "form": { + "0": { + "mci": { + "VM1": {}, + "MT2": { + "mode": "preview", + "acceptsFocus": false, + "acceptsInput": false + }, + "TM3": { + "focus": true, + "items": [ + "following", + "followers" + ] + } + }, + "actionKeys": [ + { + "keys": [ + "space" + ], + "action": "@method:spaceKeyPressed" + }, + { + "keys": [ + "down arrow", + "up arrow" + ], + "action": "@method:listKeyPressed" + }, + { + "keys": [ + "escape" + ], + "action": "@systemMethod:prevMenu" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/misc/menu_templates/main.in.hjson b/misc/menu_templates/main.in.hjson index 208892ab..755d8798 100644 --- a/misc/menu_templates/main.in.hjson +++ b/misc/menu_templates/main.in.hjson @@ -113,6 +113,10 @@ value: { command: "M" } action: @menu:messageBaseMainMenu } + { + value: { command: "A" } + action: @menu:activityPubMenu + } { value: { command: "E" } action: @menu:privateMailMenu