@@ -3,66 +3,117 @@ const vscode = require("vscode");
33/**
44 * @typedef {string } btnText selecting this option will not hide the notification in the future
55 * @typedef {string } saveAsDefaultBtnTxt selecting this option will hide the notification in the future
6+ * @typedef {Object.<string, btnText|[btnText, saveAsDefaultBtnTxt]> } Options
67 */
78
9+ /**
10+ * The Notifier class handles notifications, warnings and errors sent to the user.
11+ * Use this.createNotification() for sending a notification.
12+ */
813class Notifier {
914 /** @param {PyMakr } pymakr */
1015 constructor ( pymakr ) {
1116 this . pymakr = pymakr ;
17+ this . DONT_ASK_AGAIN = "No and don't ask again" ;
18+ this . DONT_SHOW_AGAIN = "Don't show again" ;
19+ this . messagers = {
20+ info : vscode . window . showInformationMessage ,
21+ warning : vscode . window . showWarningMessage ,
22+ error : vscode . window . showErrorMessage ,
23+ } ;
1224 }
1325
1426 /**
27+ * Creates VSCode notification, warning or error.
1528 * @example
1629 * createNotification(
1730 * 'warning', // info, warning or error
1831 * 'something happened', // the message
1932 * // buttons, the key is the return value
2033 * {
2134 * optA: 'Use Option A',
22- * // to make an option defaultable , use an array and provide the default as the second entry
35+ * // to make an option persistable , use an array and provide the persisted value as the second entry
2336 * optB: ['Use option B', 'Always use option B']
2437 * }
2538 * )
26- * @template {Object.<string, btnText|[btnText, saveAsDefaultBtnTxt]> } Buttons
39+ * @template {Options } Buttons
2740 * @param {'info'|'warning'|'error' } type
28- * @param {string } message
29- * @param {Buttons= } options
30- * @param {Boolean= } disableable
31- * @param {string= } id
32- * @returns {Promise<keyof Buttons|'disabled' > }
41+ * @param {string } message // the message shown to the user
42+ * @param {Buttons= } options // map of {key: choice} or {key: [choice, persistent choice]}
43+ * @param {Boolean= } rememberable // if true, a subsequent notification will ask if the choice should be saved for future prompts
44+ * @param {string= } id // if not set, the notification message will be used
45+ * @returns {Promise<keyof Buttons> }
3346 */
34- async createNotification ( type , message , options , disableable , id ) {
35- id = id || message ;
36- const storedValue = this . pymakr . config . get ( ) . get ( `notifications.${ id } ` ) ;
37- if ( storedValue ) return storedValue ;
47+ async createNotification ( type , message , options , rememberable , id ) {
48+ // Stale devices have question marks after stale values. We don't want these to cause duplicates
49+ id = id || message . replace ( / \? / g, "" ) ;
50+ const storedValue = this . pymakr . config . get ( ) . get ( `misc.notifications` ) [ id ] ;
51+ const messager = this . messagers [ type ] ;
52+
53+ const textToKeyMap = Object . entries ( options ) . reduce ( ( acc , [ key , arr ] ) => {
54+ [ arr ] . flat ( ) . forEach ( ( val ) => ( acc [ val ] = key ) ) ;
55+ return acc ;
56+ } , { } ) ;
3857
39- const nativeOptions = disableable ? { disabled : [ false , "Don't show again" ] } : { } ;
40- const allOptions = { ... options , ... nativeOptions } ;
58+ // if we have a stored choice, return it - except for a DONT_ASK_AGAIN value
59+ if ( storedValue && storedValue !== this . DONT_ASK_AGAIN ) return textToKeyMap [ storedValue ] ;
4160
61+ const choice = await messager ( message , ...Object . values ( options ) . flat ( ) . filter ( Boolean ) ) ;
62+
63+ const result = textToKeyMap [ choice ] ;
64+
65+ // store user's choice if wanted
66+ this . handlePersistables ( id , options , choice , rememberable ) ;
67+
68+ return result ;
69+ }
70+
71+ /**
72+ * Handles persistable logic. If a choice is a persistable it will be saved.
73+ * If it is not persistable, but the prompt is rememberable and the user makes a selection the choice will be saved.
74+ * @param {string } id
75+ * @param {Options } options
76+ * @param {string } choice
77+ * @param {Boolean } rememberable
78+ */
79+ async handlePersistables ( id , options , choice , rememberable ) {
4280 // array of button texts whose values should be saved
43- const persistables = Object . values ( allOptions )
81+ const persistables = Object . values ( options )
4482 . map ( ( text ) => Array . isArray ( text ) && text [ 1 ] )
4583 . filter ( Boolean ) ;
4684
47- const messagers = {
48- info : vscode . window . showInformationMessage ,
49- warning : vscode . window . showWarningMessage ,
50- error : vscode . window . showErrorMessage ,
51- } ;
85+ const shouldStoreChoice = persistables . includes ( choice ) || ( rememberable && ( await this . askToStore ( choice ) ) ) ;
86+ if ( shouldStoreChoice === this . DONT_ASK_AGAIN ) choice = this . DONT_ASK_AGAIN ;
87+ if ( shouldStoreChoice ) {
88+ console . log ( "setting" , `misc.notifications.${ id } ` , choice ) ;
89+ const config = this . pymakr . config . get ( ) . get ( "misc.notifications" ) ;
90+ this . pymakr . config . get ( ) . update ( `misc.notifications` , { ...config , [ id ] : choice } ) ;
91+ }
92+ }
5293
53- const messager = messagers [ type ] ;
54- const result = await messager ( message , ...Object . values ( allOptions ) . flat ( ) . filter ( Boolean ) ) ;
94+ /**
95+ * Prompts user if they want their choice to be remembered
96+ * @param {string } choice
97+ */
98+ async askToStore ( choice ) {
99+ if ( choice === undefined ) return false ;
55100
56- if ( persistables . includes ( result ) ) console . log ( "should store this thing" , result ) ;
101+ const options = {
102+ Yes : true ,
103+ [ this . DONT_ASK_AGAIN ] : this . DONT_ASK_AGAIN ,
104+ } ;
57105
58- const textToKeyMap = Object . entries ( allOptions ) . reduce ( ( acc , [ key , arr ] ) => {
59- [ arr ] . flat ( ) . forEach ( val => acc [ val ] = key )
60- return acc ;
61- } , { } ) ;
106+ const shouldSaveChoice = await vscode . window . showInformationMessage (
107+ `Do you wish to save your choice: " ${ choice } "` ,
108+ ... Object . keys ( options )
109+ ) ;
62110
63- return textToKeyMap [ result ]
111+ return options [ shouldSaveChoice ] ;
64112 }
65113
114+ /**
115+ * All available notifications in Pymakr
116+ */
66117 notifications = {
67118 showSharedTerminalInfo : ( ) =>
68119 this . createNotification (
@@ -77,16 +128,21 @@ class Notifier {
77128 this . createNotification (
78129 "info" ,
79130 `${ device . displayName } seems to be busy. Do you wish restart it in safe mode?` ,
80- { restart : "Restart in safe mode" } ,
131+ { restart : "Restart in safe mode" , [ this . DONT_SHOW_AGAIN ] : [ null , this . DONT_SHOW_AGAIN ] } ,
81132 true
82133 ) ,
83134
84135 /** @param {Device } device */
85136 terminalAlreadyExists : ( device ) =>
86- this . createNotification ( "info" , `A terminal for ${ device . displayName } already exists.` , {
87- sharedTerm : [ "Create new shared terminal" , "Always create a shared terminal" ] ,
88- openExistingTerm : [ "Open existing terminal" , "Always Open existing terminal" ] ,
89- } ) ,
137+ this . createNotification (
138+ "info" ,
139+ `A terminal for ${ device . displayName } already exists.` ,
140+ {
141+ sharedTerm : "Create new shared terminal" ,
142+ openExistingTerm : "Open existing terminal" ,
143+ } ,
144+ true
145+ ) ,
90146
91147 /** @param {Project } project */
92148 couldNotParsePymakrConfig : ( project ) =>
@@ -118,7 +174,8 @@ class Notifier {
118174 couldNotSafeboot : ( device ) =>
119175 this . createNotification (
120176 "warning" ,
121- "Could not safeboot device. Please hard reset the device and verify that a shield is installed."
177+ "Could not safeboot device. Please hard reset the device and verify that a shield is installed." ,
178+ { [ this . DONT_SHOW_AGAIN ] : [ null , this . DONT_SHOW_AGAIN ] }
122179 ) ,
123180 } ;
124181}
0 commit comments