Merge pull request #377 from ihabunek/settings
Implement a settings file
This commit is contained in:
commit
d71cc7e3b6
|
@ -5,6 +5,7 @@
|
||||||
- [Installation](installation.md)
|
- [Installation](installation.md)
|
||||||
- [Usage](usage.md)
|
- [Usage](usage.md)
|
||||||
- [Advanced](advanced.md)
|
- [Advanced](advanced.md)
|
||||||
|
- [Settings](settings.md)
|
||||||
- [TUI](tui.md)
|
- [TUI](tui.md)
|
||||||
- [Contributing](contributing.md)
|
- [Contributing](contributing.md)
|
||||||
- [Documentation](documentation.md)
|
- [Documentation](documentation.md)
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Settings
|
||||||
|
|
||||||
|
Toot can be configured via a [TOML](https://toml.io/en/) settings file.
|
||||||
|
|
||||||
|
> Introduced in toot 0.37.0
|
||||||
|
|
||||||
|
> **Warning:** Settings are experimental and things may change without warning.
|
||||||
|
|
||||||
|
Toot will look for the settings file at:
|
||||||
|
|
||||||
|
* `~/.config/toot/settings.toml` (Linux & co.)
|
||||||
|
* `%APPDATA%\toot\settings.toml` (Windows)
|
||||||
|
|
||||||
|
Toot will respect the `XDG_CONFIG_HOME` environement variable if it's set and
|
||||||
|
look for the settings file in `$XDG_CONFIG_HOME/toot` instead of
|
||||||
|
`~/.config/toot`.
|
||||||
|
|
||||||
|
## Common options
|
||||||
|
|
||||||
|
The `[common]` section includes common options which are applied to all commands.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[common]
|
||||||
|
# Whether to use ANSI color in output
|
||||||
|
color = true
|
||||||
|
|
||||||
|
# Enable debug logging, shows HTTP requests
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
# Redirect debug log to the given file
|
||||||
|
debug_file = "/tmp/toot.log"
|
||||||
|
|
||||||
|
# Log request and response bodies in the debug log
|
||||||
|
verbose = false
|
||||||
|
|
||||||
|
# Do not write to output
|
||||||
|
quiet = false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overriding command defaults
|
||||||
|
|
||||||
|
Defaults for command arguments can be override by specifying a `[command.<name>]` section.
|
||||||
|
|
||||||
|
For example, to override `toot post`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[command.post]
|
||||||
|
editor = "vim"
|
||||||
|
sensitive = true
|
||||||
|
visibility = "unlisted"
|
||||||
|
scheduled_in = "30 minutes"
|
||||||
|
```
|
1
setup.py
1
setup.py
|
@ -38,6 +38,7 @@ setup(
|
||||||
"beautifulsoup4>=4.5.0,<5.0",
|
"beautifulsoup4>=4.5.0,<5.0",
|
||||||
"wcwidth>=0.1.7",
|
"wcwidth>=0.1.7",
|
||||||
"urwid>=2.0.0,<3.0",
|
"urwid>=2.0.0,<3.0",
|
||||||
|
"tomlkit>=0.10.0,<1.0"
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
|
|
|
@ -8,7 +8,7 @@ To enable integration tests, export the following environment variables to match
|
||||||
your test server and database:
|
your test server and database:
|
||||||
|
|
||||||
```
|
```
|
||||||
export TOOT_TEST_HOSTNAME="localhost:3000"
|
export TOOT_TEST_BASE_URL="localhost:3000"
|
||||||
export TOOT_TEST_DATABASE_DSN="dbname=mastodon_development"
|
export TOOT_TEST_DATABASE_DSN="dbname=mastodon_development"
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
@ -26,6 +26,11 @@ from toot.exceptions import ApiError, ConsoleError
|
||||||
from toot.output import print_out
|
from toot.output import print_out
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
import toot.settings
|
||||||
|
toot.settings.DISABLE_SETTINGS = True
|
||||||
|
|
||||||
|
|
||||||
# Mastodon database name, used to confirm user registration without having to click the link
|
# Mastodon database name, used to confirm user registration without having to click the link
|
||||||
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
DATABASE_DSN = os.getenv("TOOT_TEST_DATABASE_DSN")
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from os.path import join, expanduser
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
__version__ = '0.37.0'
|
__version__ = '0.37.0'
|
||||||
|
@ -9,3 +13,22 @@ DEFAULT_INSTANCE = 'https://mastodon.social'
|
||||||
|
|
||||||
CLIENT_NAME = 'toot - a Mastodon CLI client'
|
CLIENT_NAME = 'toot - a Mastodon CLI client'
|
||||||
CLIENT_WEBSITE = 'https://github.com/ihabunek/toot'
|
CLIENT_WEBSITE = 'https://github.com/ihabunek/toot'
|
||||||
|
|
||||||
|
TOOT_CONFIG_DIR_NAME = "toot"
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_dir():
|
||||||
|
"""Returns the path to toot config directory"""
|
||||||
|
|
||||||
|
# On Windows, store the config in roaming appdata
|
||||||
|
if sys.platform == "win32" and "APPDATA" in os.environ:
|
||||||
|
return join(os.getenv("APPDATA"), TOOT_CONFIG_DIR_NAME)
|
||||||
|
|
||||||
|
# Respect XDG_CONFIG_HOME env variable if set
|
||||||
|
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||||
|
if "XDG_CONFIG_HOME" in os.environ:
|
||||||
|
config_home = expanduser(os.environ["XDG_CONFIG_HOME"])
|
||||||
|
return join(config_home, TOOT_CONFIG_DIR_NAME)
|
||||||
|
|
||||||
|
# Default to ~/.config/toot/
|
||||||
|
return join(expanduser("~"), ".config", TOOT_CONFIG_DIR_NAME)
|
||||||
|
|
|
@ -1,44 +1,22 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from os.path import dirname, join, expanduser
|
from os.path import dirname, join
|
||||||
|
|
||||||
from toot import User, App
|
from toot import User, App, get_config_dir
|
||||||
from toot.exceptions import ConsoleError
|
from toot.exceptions import ConsoleError
|
||||||
from toot.output import print_out
|
from toot.output import print_out
|
||||||
|
|
||||||
|
|
||||||
TOOT_CONFIG_DIR_NAME = "toot"
|
|
||||||
TOOT_CONFIG_FILE_NAME = "config.json"
|
TOOT_CONFIG_FILE_NAME = "config.json"
|
||||||
|
|
||||||
|
|
||||||
def get_config_dir():
|
|
||||||
"""Returns the path to toot config directory"""
|
|
||||||
|
|
||||||
# On Windows, store the config in roaming appdata
|
|
||||||
if sys.platform == "win32" and "APPDATA" in os.environ:
|
|
||||||
return join(os.getenv("APPDATA"), TOOT_CONFIG_DIR_NAME)
|
|
||||||
|
|
||||||
# Respect XDG_CONFIG_HOME env variable if set
|
|
||||||
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
||||||
if "XDG_CONFIG_HOME" in os.environ:
|
|
||||||
config_home = expanduser(os.environ["XDG_CONFIG_HOME"])
|
|
||||||
return join(config_home, TOOT_CONFIG_DIR_NAME)
|
|
||||||
|
|
||||||
# Default to ~/.config/toot/
|
|
||||||
return join(expanduser("~"), ".config", TOOT_CONFIG_DIR_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
def get_config_file_path():
|
def get_config_file_path():
|
||||||
"""Returns the path to toot config file."""
|
"""Returns the path to toot config file."""
|
||||||
return join(get_config_dir(), TOOT_CONFIG_FILE_NAME)
|
return join(get_config_dir(), TOOT_CONFIG_FILE_NAME)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_FILE = get_config_file_path()
|
|
||||||
|
|
||||||
|
|
||||||
def user_id(user):
|
def user_id(user):
|
||||||
return "{}@{}".format(user.username, user.instance)
|
return "{}@{}".format(user.username, user.instance)
|
||||||
|
|
||||||
|
@ -63,15 +41,18 @@ def make_config(path):
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
if not os.path.exists(CONFIG_FILE):
|
path = get_config_file_path()
|
||||||
make_config(CONFIG_FILE)
|
|
||||||
|
|
||||||
with open(CONFIG_FILE) as f:
|
if not os.path.exists(path):
|
||||||
|
make_config(path)
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
def save_config(config):
|
def save_config(config):
|
||||||
with open(CONFIG_FILE, 'w') as f:
|
path = get_config_file_path()
|
||||||
|
with open(path, "w") as f:
|
||||||
return json.dump(config, f, indent=True, sort_keys=True)
|
return json.dump(config, f, indent=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,10 @@ import sys
|
||||||
from argparse import ArgumentParser, FileType, ArgumentTypeError, Action
|
from argparse import ArgumentParser, FileType, ArgumentTypeError, Action
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from toot import config, commands, CLIENT_NAME, CLIENT_WEBSITE, __version__
|
from toot import config, commands, CLIENT_NAME, CLIENT_WEBSITE, __version__, settings
|
||||||
from toot.exceptions import ApiError, ConsoleError
|
from toot.exceptions import ApiError, ConsoleError
|
||||||
from toot.output import print_out, print_err
|
from toot.output import print_out, print_err
|
||||||
|
from toot.settings import get_setting
|
||||||
|
|
||||||
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"]
|
||||||
VISIBILITY_CHOICES_STR = ", ".join(f"'{v}'" for v in VISIBILITY_CHOICES)
|
VISIBILITY_CHOICES_STR = ", ".join(f"'{v}'" for v in VISIBILITY_CHOICES)
|
||||||
|
@ -882,12 +883,24 @@ def get_argument_parser(name, command):
|
||||||
if command.require_auth:
|
if command.require_auth:
|
||||||
combined_args += common_auth_args
|
combined_args += common_auth_args
|
||||||
|
|
||||||
|
defaults = get_setting(f"commands.{name}", dict, {})
|
||||||
|
|
||||||
for args, kwargs in combined_args:
|
for args, kwargs in combined_args:
|
||||||
|
# Set default value from settings if exists
|
||||||
|
default = get_default_value(defaults, args)
|
||||||
|
if default is not None:
|
||||||
|
kwargs["default"] = default
|
||||||
parser.add_argument(*args, **kwargs)
|
parser.add_argument(*args, **kwargs)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_value(defaults, args):
|
||||||
|
# Hacky way to determine command name from argparse args
|
||||||
|
name = args[-1].lstrip("-").replace("-", "_")
|
||||||
|
return defaults.get(name)
|
||||||
|
|
||||||
|
|
||||||
def run_command(app, user, name, args):
|
def run_command(app, user, name, args):
|
||||||
command = next((c for c in COMMANDS if c.name == name), None)
|
command = next((c for c in COMMANDS if c.name == name), None)
|
||||||
|
|
||||||
|
@ -919,9 +932,8 @@ def run_command(app, user, name, args):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Enable debug logging if --debug is in args
|
if settings.get_debug():
|
||||||
if "--debug" in sys.argv:
|
filename = settings.get_debug_file()
|
||||||
filename = os.getenv("TOOT_LOG_FILE")
|
|
||||||
logging.basicConfig(level=logging.DEBUG, filename=filename)
|
logging.basicConfig(level=logging.DEBUG, filename=filename)
|
||||||
logging.getLogger("urllib3").setLevel(logging.INFO)
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
from toot import settings
|
||||||
from toot.entities import Instance, Notification, Poll, Status
|
from toot.entities import Instance, Notification, Poll, Status
|
||||||
from toot.utils import get_text, parse_html
|
from toot.utils import get_text, parse_html
|
||||||
from toot.wcstring import wc_wrap
|
from toot.wcstring import wc_wrap
|
||||||
|
@ -99,6 +101,7 @@ def strip_tags(message):
|
||||||
return re.sub(STYLE_TAG_PATTERN, "", message)
|
return re.sub(STYLE_TAG_PATTERN, "", message)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
def use_ansi_color():
|
def use_ansi_color():
|
||||||
"""Returns True if ANSI color codes should be used."""
|
"""Returns True if ANSI color codes should be used."""
|
||||||
|
|
||||||
|
@ -115,23 +118,24 @@ def use_ansi_color():
|
||||||
if "--no-color" in sys.argv:
|
if "--no-color" in sys.argv:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check in settings
|
||||||
|
color = settings.get_setting("common.color", bool)
|
||||||
|
if color is not None:
|
||||||
|
return color
|
||||||
|
|
||||||
|
# Use color by default
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
USE_ANSI_COLOR = use_ansi_color()
|
|
||||||
|
|
||||||
QUIET = "--quiet" in sys.argv
|
|
||||||
|
|
||||||
|
|
||||||
def print_out(*args, **kwargs):
|
def print_out(*args, **kwargs):
|
||||||
if not QUIET:
|
if not settings.get_quiet():
|
||||||
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
|
args = [colorize(a) if use_ansi_color() else strip_tags(a) for a in args]
|
||||||
print(*args, **kwargs)
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def print_err(*args, **kwargs):
|
def print_err(*args, **kwargs):
|
||||||
args = [f"<red>{a}</red>" for a in args]
|
args = [f"<red>{a}</red>" for a in args]
|
||||||
args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args]
|
args = [colorize(a) if use_ansi_color() else strip_tags(a) for a in args]
|
||||||
print(*args, file=sys.stderr, **kwargs)
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
from os.path import exists, join
|
||||||
|
from tomlkit import parse
|
||||||
|
from toot import get_config_dir
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
|
||||||
|
DISABLE_SETTINGS = False
|
||||||
|
|
||||||
|
TOOT_SETTINGS_FILE_NAME = "settings.toml"
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings_path():
|
||||||
|
return join(get_config_dir(), TOOT_SETTINGS_FILE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings() -> dict:
|
||||||
|
# Used for testing without config file
|
||||||
|
if DISABLE_SETTINGS:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
path = get_settings_path()
|
||||||
|
|
||||||
|
if not exists(path):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
|
return parse(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def get_settings():
|
||||||
|
return load_settings()
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting(key: str, type: Type, default=None):
|
||||||
|
"""
|
||||||
|
Get a setting value. The key should be a dot-separated string,
|
||||||
|
e.g. "commands.post.editor" which will correspond to the "editor" setting
|
||||||
|
inside the `[commands.post]` section.
|
||||||
|
"""
|
||||||
|
settings = get_settings()
|
||||||
|
return _get_setting(settings, key.split("."), type, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_setting(dct, keys, type: Type, default=None):
|
||||||
|
if len(keys) == 0:
|
||||||
|
if isinstance(dct, type):
|
||||||
|
return dct
|
||||||
|
else:
|
||||||
|
# TODO: warn? cast? both?
|
||||||
|
return default
|
||||||
|
|
||||||
|
key = keys[0]
|
||||||
|
if isinstance(dct, dict) and key in dct:
|
||||||
|
return _get_setting(dct[key], keys[1:], type, default)
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug() -> bool:
|
||||||
|
if "--debug" in sys.argv:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return get_setting("common.debug", bool, False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_debug_file() -> Optional[str]:
|
||||||
|
from_env = os.getenv("TOOT_LOG_FILE")
|
||||||
|
if from_env:
|
||||||
|
return from_env
|
||||||
|
|
||||||
|
return get_setting("common.debug_file", str)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def get_quiet():
|
||||||
|
if "--quiet" in sys.argv:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return get_setting("common.quiet", str, False)
|
Loading…
Reference in New Issue