Repository: pimutils/khal
Pull request #615 (2017-03-26 12:35:24)

Streamline config wizard
I really like khal, but I thought the initial setup was a bit fiddly. I guess most experienced users aren't going through the config wizard very often, so I thought I'd have a go at simplifying it.

- In master, a typical setup took 8 prompts and 15 keypresses
- The first commit here removes some of the intermediate confirmation prompts (if you get it wrong, you can just start over), and changes the final confirmation prompt to default to yes.
  - 6 prompts, 10 keypresses
- The second commit (which might be more controversial) condenses picking the date format into a single set of options I guess are most likely: `%Y-%m-%d, %d/%m/%Y, %m/%d/%Y`
  - 5 prompts, 8 keypresses

I think the first step is a clear improvement. The second is a trade-off between simplicity and flexibility. I think it's a better user experience with one fewer choice to think about, and users who really want another format can still set that in the config file.

Here's what it looks like after both commits:

```
$ khal configure
What ordering of year, month, date do you want to use?
[0] year-month-day (today: 2017-03-26)
[1] day/month/year (today: 26/03/2017)
[2] month/day/year (today: 03/26/2017)
Please choose one of the above options: 1
Date format: %d/%m/%Y (today as an example: 26/03/2017)

What timeformat do you want to use?
[0] 24 hour clock (recommended)
[1] 12 hour clock
Please choose one of the above options [0]:
Time format: %H:%M (current time as an example: 13:13)

If you use vdirsyncer to sync with CalDAV servers, we can try to load its config file and add your calendars to khal's config.
Should we try to load vdirsyncer's config? [Y/n]: n

Do you want to create a local calendar? (You can always set it up to synchronize with a server in vdirsyncer later). [y/N]: y
Created new vdir at /home/takluyver/.local/share/khal/calendars/private111
Do you want to write the config to /home/takluyver/.config/khal/config? (Choosing `No` will abort) [Y/n]:
Successfully wrote configuration to /home/takluyver/.config/khal/config
```


Patch:
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 99b7bdb..828bc11 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -30,3 +30,4 @@ Paweł Fertyk - pfertyk [at] openmailbox [dot] org
 Moritz Kobel - moritz [at] kobelnet [dot] ch - http://www.kobelnet.ch
 Guilhem Saurel - guilhem [at] saurel [dot] me - https://saurel.me
 Stefan Siegel - ssiegel [at] sdas [dot] net
+Thomas Kluyver - thomas [at] kluyver [dot] me [dot] uk
diff --git a/khal/configwizard.py b/khal/configwizard.py
index 4d8b86b..fd0b8c0 100644
--- a/khal/configwizard.py
+++ b/khal/configwizard.py
@@ -24,6 +24,7 @@ from click import confirm, prompt, UsageError
 import xdg
 
 from functools import partial
+from itertools import zip_longest
 from os.path import expanduser, expandvars, join, normpath, exists, isdir
 from os import makedirs
 
@@ -45,50 +46,66 @@ def validate_int(input, min_value, max_value):
         raise UsageError('Input must be between {} and {}'.format(min_value, max_value))
 
 
+DATE_FORMAT_INFO = [
+    ('Year', ['%Y', '%y']),
+    ('Month', ['%m', '%B', '%b']),
+    ('Day', ['%d', '%a', '%A'])
+]
+
+
+def present_date_format_info(example_date):
+    columns = []
+    widths = []
+    for title, formats in DATE_FORMAT_INFO:
+        newcol = [title]
+        for f in formats:
+            newcol.append('{}={}'.format(f, example_date.strftime(f)))
+        widths.append(max(len(s) for s in newcol) + 2)
+        columns.append(newcol)
+
+    print('Common fields for date formatting:')
+    for row in zip_longest(*columns, fillvalue=''):
+        print(''.join(s.ljust(w) for (s, w) in zip(row, widths)))
+
+    print('More info: '
+          'https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior')
+
+
 def choose_datetime_format():
     """query user for their date format of choice"""
-    while True:
-        ordering_choices = [
-            ('year month day', ['%Y', '%m', '%d']),
-            ('day month year', ['%d', '%m', '%Y']),
-            ('month day year', ['%m', '%d', '%Y']),
-        ]
-        separator_choices = ['-', '.', '/']
-        validate = partial(validate_int, min_value=0, max_value=2)
-
-        print("What ordering of year, month, date do you want to use? "
-              "(You can choose the separator in the next step)")
-        print('\n'.join(
-            ['[{}] {}'.format(num, one) for num, (one, _) in enumerate(ordering_choices)]))
-        ordering_no = prompt("Please choose one of the above options", value_proc=validate)
-        print()
-        print("Now, please choose a separator")
-        print('\n'.join(['[{}] {}'.format(num, one) for num, one in enumerate(separator_choices)]))
-        prompt_text = "Please choose one of the above options"
-        separator = separator_choices[prompt(prompt_text, value_proc=validate)]
-        dateformat = separator.join(ordering_choices[ordering_no][1])
-        today = date.today()
-        text = ("Does this look sensible to you: {} "
-                "(today as an example: {})?".format(dateformat, today.strftime(dateformat)))
-        if confirm(text):
-            break
+    choices = [
+        ('year-month-day', '%Y-%m-%d'),
+        ('day/month/year', '%d/%m/%Y'),
+        ('month/day/year', '%m/%d/%Y'),
+    ]
+    validate = partial(validate_int, min_value=0, max_value=3)
+    today = date.today()
+    print("What ordering of year, month, date do you want to use?")
+    for num, (desc, fmt) in enumerate(choices):
+        print('[{}] {} (today: {})'.format(num, desc, today.strftime(fmt)))
+    print('[3] Custom')
+    choice_no = prompt("Please choose one of the above options", value_proc=validate)
+    if choice_no == 3:
+        present_date_format_info(today)
+        dateformat = prompt('Make your date format')
+    else:
+        dateformat = choices[choice_no][1]
+    print("Date format: {} "
+          "(today as an example: {})".format(dateformat, today.strftime(dateformat)))
     return dateformat
 
 
 def choose_time_format():
     """query user for their time format of choice"""
     choices = ['%H:%M', '%I:%M %p']
-    while True:
-        print("What timeformat do you want to use?")
-        print("[0] 24 hour clock (recommended)\n[1] 12 hour clock")
-        validate = partial(validate_int, min_value=0, max_value=1)
-        prompt_text = "Please choose one of the above options"
-        timeformat = choices[prompt(prompt_text, default=0, value_proc=validate)]
-        now = datetime.now()
-        text = ("Does this look sensible to you: {} "
-                "(current time as an example: {})?".format(timeformat, now.strftime(timeformat)))
-        if confirm(text):
-            break
+    print("What timeformat do you want to use?")
+    print("[0] 24 hour clock (recommended)\n[1] 12 hour clock")
+    validate = partial(validate_int, min_value=0, max_value=1)
+    prompt_text = "Please choose one of the above options"
+    timeformat = choices[prompt(prompt_text, default=0, value_proc=validate)]
+    now = datetime.now()
+    print("Time format: {} "
+          "(current time as an example: {})".format(timeformat, now.strftime(timeformat)))
     return timeformat
 
 
@@ -197,7 +214,7 @@ def configwizard():
     config_path = join(xdg.BaseDirectory.xdg_config_home, 'khal', 'config')
     if not confirm(
             "Do you want to write the config to {}? "
-            "(Choosing `No` will abort)".format(config_path)):
+            "(Choosing `No` will abort)".format(config_path), default=True):
         raise FatalError('User aborted...')
     config_dir = join(xdg.BaseDirectory.xdg_config_home, 'khal')
     if not exists(config_dir) and not isdir(config_dir):


Test patch:
diff --git a/tests/cli_test.py b/tests/cli_test.py
index 1191784..4950e6a 100644
--- a/tests/cli_test.py
+++ b/tests/cli_test.py
@@ -505,8 +505,7 @@ def test_color_option(runner):
     assert result.output != 'No events\n'
 
 
-def choices(ordering=0, separator=0, dateconfirm=True,
-            timeformat=0, timeconfirm=True,
+def choices(dateformat=0, timeformat=0,
             parse_vdirsyncer_conf=True,
             create_vdir=False,
             write_config=True):
@@ -514,8 +513,7 @@ def choices(ordering=0, separator=0, dateconfirm=True,
     confirm = {True: 'y', False: 'n'}
 
     out = [
-        str(ordering), str(separator), confirm[dateconfirm],
-        str(timeformat), confirm[timeconfirm],
+        str(dateformat), str(timeformat),
         confirm[parse_vdirsyncer_conf],
     ]
     if not parse_vdirsyncer_conf: