The Village - Justin - Information


ChoicesDoc - How to use Choices: correctly

  1. Overview
  2. This document details how you should read and write choices in your applications. Correctly implemented, choices should be able to be used on Pre-RPC and RPC systems, as well as persisting across an upgrade.

  3. Implementation
  4. The Choices: path and Choices$Write variables are one of the worst used parts of the current boot system. Not because nobody uses it, but because very few of those people who do use it, use it correctly.

    Let's start with some fundamentals :

    Choices$Path
    This is a path variable for reading the configuration files for applications. The general concensus is that there is one directory or file for each application, named the same thing as the application, without the leading !.

    You may not write to files in with the path Choices:*.

    Choices$Write
    This is a directory variable for modifying the configuration files of applications.

    You may not read from files in the path <Choices$Write>.*.

    So, we have one variable to read from and one to write to. Why ?

    Very simple really. This way it is possible to have 'global' defaults which are set for all users of a system, and personal defaults used by each user. Ideally, Choices$Path is set to point first to the user's Choices directory, followed by the global one, and Choices$Write is set to the user's Choices directory.

    Ok, now a simple example :

    We have a program !DoNowt, which wishes to store its configuration details - a few switches - as a binary file.

    When it starts, !DoNowt will do the equivilent of :

      i%=OPENIN("Choices:DoNowt") :REM Open the file
      IF i%=0 THEN
       REM The file does not exist - use defaults
       choices=default_value
      ELSE
       REM The file exists - use those in the file
       choices=BGET#i%
       CLOSE#i%
      ENDIF
    

    To read its configuration. But of course that isn't backward compatible with non-RPC boot structures. So, how do we get around that ? Like this :

      SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
      IF read<0 THENfile$="Choices:DoNowt" ELSEfile$="<DoNowt$Dir>.Choices"
      i%=OPENIN(file$)
      etc...
    

    Which will allow it to write to its own directory if it runs on a non-RPC Boot structured machine.

    But what about writing ? That is similarly easy, and we can just steal the same code pretty much :

      SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
      IF read<0 THENfile$="<Choices$Write>.DoNowt" ELSEfile$="<DoNowt$Dir>.Choices"
      o%=OPENOUT(file$)
      IF o%=0 THEN
       REM The file couldn't be opened - directory/disc full ?
       ERROR 0,"Could not write choices file, dying horribly"
      ELSE
       REM The file was opened - write the data
       BPUT#o%,choices
       CLOSE#o%
      ENDIF
    

    Which is vaguely entertaining. Obviously the error should be reported differently, and should at worst simply do nothing as if the options had been written correctly.

    Now consider that someone has upgraded their machine from non-RPC style boot sequence to one that is. This presents a problem. We have to auto-upgrade. But we can do that whilst reading the files :

      SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
      IF read<0 THEN
       REM The variable exists - check for auto-upgrade
       file$="Choices:DoNowt"
       SYS "XOS_File",5,"<DoNowt$Dir>.Choices" TO ;flags
       IF (flags AND 1)=0 THEN
        REM The file exists, so we should move it to the new structure
        SYS "XOS_FSControl",26,"<DoNowt$Dir>.Choices","<Choices$Write>.DoNowt",
             (1<<7)+(1<<1) TO ;flags
        IF (flags AND 1) THENERROR 0,"Could not auto-upgrade the choices file"
       ENDIF
      ELSE
       file$="<DoNowt$Dir>.Choices"
      ENDIF
      i%=OPENIN(file$)
      etc...
    

    This will move the file from the application directory to the users choices directory. This will be in the Choices: path and will therefore be read next. Notice that we don't copy to Choices: as this may have multiple entries. This technique requires that your application is distributed without a choices file. If you wish to include a default choices file you should give this another name within the application and if the users choices file did not exist read that :

      i%=OPENIN(file$)
      IF i%=0 THENi%=OPENIN("<DoNowt$Dir>.Defaults")
      IF i%=0 THEN
       REM The defaults choices file does not exist either - generate defaults
       choices=default_value
      ELSE
       REM The file exists - use those in the file
       choices=BGET#i%
       CLOSE#i%
      ENDIF
    

    The 'generate defaults' section can easily be omited if you feel that the user should never touch the inside of your application.

    So, what do we have ? Well, we have an application which will load its options from the Choices system if a RPC boot sequence is installed, or its own directory if not. It will write back to the correct place for the user configuration. It is NOT up to the application to provide multi-user support except in exceptional situations - this is the point of Choices.

    Ok, so lets see the code we have so far. When reading a file :

      SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
      IF read<0 THEN
       REM The variable exists - check for auto-upgrade
       file$="Choices:DoNowt"
       SYS "XOS_File",5,"<DoNowt$Dir>.Choices" TO ;flags
       IF (flags AND 1)=0 THEN
        REM The file exists, so we should move it to the new structure
        SYS "XOS_FSControl",26,"<DoNowt$Dir>.Choices","<Choices$Write>.DoNowt",
             (1<<7)+(1<<1) TO ;flags
        IF (flags AND 1) THENERROR 0,"Could not auto-upgrade the choices file"
       ENDIF
      ELSE
       file$="<DoNowt$Dir>.Choices"
      ENDIF
      i%=OPENIN(file$)
      IF i%=0 THENi%=OPENIN("<DoNowt$Dir>.Defaults")
      IF i%=0 THEN
       REM The defaults choices file does not exist either - generate defaults
       choices=default_value
      ELSE
       REM The file exists - use those in the file
       choices=BGET#i%
       CLOSE#i%
      ENDIF
    

    When writing a file :

      SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
      IF read<0 THENfile$="<Choices$Write>.DoNowt" ELSEfile$="<DoNowt$Dir>.Choices"
      o%=OPENOUT(file$)
      IF o%=0 THEN
       REM The file couldn't be opened - directory/disc full ?
       ERROR 0,"Could not write choices file, dying horribly"
      ELSE
       REM The file was opened - write the data
       BPUT#o%,choices
       CLOSE#o%
      ENDIF
    

    This is all well and good, but it doesn't cope with the times when we need to store many files in the Choices structure - say the choices from different areas of the program. This means we have to think a little more. The best way to sort this out is to have a function that returns the filename we should use for a particular file.

      DEFFNchoicesfile(app$,name$,write)
      LOCAL read,file$,flags
      SYS "XOS_ReadVarVal","Choices$Write",-1,-1,0,0 TO,,read
      IF read<0 THEN
       REM The variable exists
       IF write THEN
        REM We're writing to the file
        file$="<Choices$Write>."+app$+"."+name$
        REM Ensure the direcotry exists
        SYS "XOS_File",8,"<Choices$Write>."+app$,,,0
       ELSE
        REM We're reading the file
        file$="Choices:"+app$+"."+name$
       ENDIF
       REM Check for an auto-upgrade
       SYS "XOS_File",5,"<"+app$+"$Dir>.Choices."+name$ TO ;flags
       IF (flags AND 1)=0 THEN
        REM The file exists, so we should move it to the new structure...
        IF write THEN
         REM If we're writing the file, then the one we would copy is out of date
         REM so we delete it
         SYS "XOS_File",6,"<"+apps$+"$Dir>.Choices."+name$
        ELSE
         REM We are reading, upgrade the files - need to create the directory first
         SYS "XOS_File",8,"<Choices$Write>."+app$,,,0
         REM Now we move the file
         SYS "XOS_FSControl",26,"<"+apps$+"$Dir>.Choices."+name$,
                                "<Choices$Write>."+app$+"."+name$,
              (1<<7)+(1<<1) TO ;flags
         IF (flags AND 1) THEN
          REM Could not auto-upgrade the choices file - delete it!
          ERROR 0,"Could not auto-upgrade the choices file"
         ENDIF
        ENDIF
       ENDIF
      ELSE
       file$="<"+app$+"$Dir>.Choices."+name$
      ENDIF
      =file$
    

    Notice that this deals with almost every eventuality. If there is an error during the auto-upgrade then an error will be generated. I'm sure you can think of a suitable fallback method in this situation. The easiest thing to do is just delete the original.

    Remember that the function above only finds the name of the file containing our options. However, it does simplify our code a little. For reading :

      file$=FNchoicesfile("DoNowt","Choices",FALSE):REM Note: FALSE
      i%=OPENIN(file$)
      IF i%=0 THEN
       REM We just want to know the defaults
       choices=default_value
      ELSE
       REM The file exists - use those in the file
       choices=BGET#i%
       CLOSE#i%
      ENDIF
    

    This removes the option of the defaults file. This can easily be added as it was above.

    For writing :

      file$=FNchoicesfile("DoNowt","Choices",TRUE):REM Note: TRUE
      o%=OPENOUT(file$)
      IF o%=0 THEN
       REM The file couldn't be opened - directory/disc full ?
       ERROR 0,"Could not write choices file, dying horribly"
      ELSE
       REM The file was opened - write the data
       BPUT#o%,choices
       CLOSE#o%
      ENDIF
    

    If I've written that all correctly then you should be able to write an application very easily which will not only work on RISC OS 3.1 through to RISC OS 3.7 in both networked and stand-alone environments, but also will handle the user upgrading their machine completely transparently, and running from a ReadOnly media such as CD-ROM or on a network mounted filestore.

  5. 'Grouping' applications
  6. In the example above I have dealt with a single application storing its options. You may feel that it would be reasonable to store files for all your applications in one place. At least one author is currently doing this, and it reduces the problem of the 77 file per directory limit. 'New' FileCore will be available in RISC OS 4, but it is likely that it will not be available for older computers. Choices:<author>.<application>.<file> may seem a little longwinded, but it helps to reduce the number of objects in the root of Choices:. In this case I recommend that you use your initial and surname, eg JFletcher.

    A better alternative for some programs may be to use a category directory. Acorn themselves have started the ball rolling on this with a 'WWW' directory for all Web related resources. Again, this will only work if people use it. To my knowledge only three category directories are in use at present :

    IRC
    All IRC related applications should store their details here. Many clients currently do not, but it should not be difficult to auto-upgrade from their own directory to the IRC directory.
    WWW
    All Web related applications should store their details here.
    !ZapUser
    Zap modules and resources for the user live here.

    It is not necessary to create a new category if the need is specialised and there are not likely to be more applications making use of it. If you feel there is a need for a new category you may wish to contact Acorn or other developers to confirm or deny any need.

  7. Registration of names
  8. When you register an application name (and have had it confirmed!), you also have an allocation in Choices: of the same name. Bare this in mind when creating your application.

  9. Contents of files
  10. Now the only question that remains is what to store in your configuration files. Obviously the configuration details, but I would suggest that you store them in textual format in colon delimited field format (ie like MessageTrans files). Why would you want to do this ? Well, it makes it easier on you to see what is going on in the configuration system, and for the user to modify if they need to. It is, of course, your decision as to how you store your configuration details :-)

  11. Robustness
  12. As a word of warning, no application should ever crash because of a poorly formated configuration file. Loading an entire file into a structure (in C) is likely to cause problems if the file is corrupt in any way. Reading textual strings in BASIC should be checked for overly long strings. Non-existant tags (in tagged files) should be given default values. I'd recommend that you simply ignore any 'broken' configuration file (possibly telling the user) and use the default choices.

  13. When to write choices
  14. In general, you should not write choices unless the user asks you to. This is not practical in some situations, particularly where the choices require a reload to take effect or where there are so few that it is not sensible to include a 'Save' option.

    In particular you should not write choices when your application quits. Doing so may cause invalid choices to be written, or (in the case of defaulted choices) overwrite the choices. The latter case should be avoided, as the user may have been able to 'fix' the problem (or have caused it in the first place!).

  15. Comments
  16. Comments about this document should be directed to the author, Justin Fletcher. You can, of course, ignore any part of this document.

  17. History
  18. Version 0.00 : 04 Jun 1998
    Released to various people via IRC and email for comments. Sent to Dave Walker for confirmation that I'm not talking rubbish :-)
    Version 1.00 : 29 Jun 1998
    Suggestions, comments and complaints added to the document. Reformated into sections.
    Version 1.01 : 08 Jan 1999
    Reformatted as HTML for the 'info' section of my website.

This page is maintained by Justin Fletcher (gerph@thevillage.ndirect.co.uk).
Last modified on 28 December, 1999.