diff --git a/README.rst b/README.rst
index a041f2d..2501705 100644
--- a/README.rst
+++ b/README.rst
@@ -283,6 +283,7 @@ The following Miscellaneous utilities are available as helper methods:
     * ``vistir.misc.partialclass``
     * ``vistir.misc.to_text``
     * ``vistir.misc.to_bytes``
+    * ``vistir.misc.decode_for_output``
 
 
 .. _`shell_escape`:
@@ -397,6 +398,16 @@ Converts arbitrary byte-convertable input to bytes while handling errors.
     b'this is some text'
 
 
+.. _`decode_for_output`:
+
+**decode_for_output**
+//////////////////////
+
+Converts an arbitrary text input to output which is encoded for printing to terminal
+outputs using the system preferred locale using ``locale.getpreferredencoding(False)``
+with some additional hackery on linux systems.
+
+
 🐉 Path Utilities
 ------------------
 
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index dac6f99..418622b 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -283,6 +283,7 @@ The following Miscellaneous utilities are available as helper methods:
     * :func:`~vistir.misc.partialclass`
     * :func:`~vistir.misc.to_text`
     * :func:`~vistir.misc.to_bytes`
+    * :func:`~vistir.misc.decode_for_output`
 
 
 .. _`shell_escape`:
@@ -397,6 +398,21 @@ Converts arbitrary byte-convertable input to bytes while handling errors.
     b'this is some text'
 
 
+.. _`decode_for_output`:
+
+**decode_for_output**
+//////////////////////
+
+Converts an arbitrary text input to output which is encoded for printing to terminal
+outputs using the system preferred locale using ``locale.getpreferredencoding(False)``
+with some additional hackery on linux systems.
+
+.. code:: python
+
+    >>> vistir.misc.decode_for_output(u"Some text")
+    "some default locale encoded text"
+
+
 🐉 Path Utilities
 ------------------
 
diff --git a/news/32.bugfix.rst b/news/32.bugfix.rst
new file mode 100644
index 0000000..4bb31b3
--- /dev/null
+++ b/news/32.bugfix.rst
@@ -0,0 +1,1 @@
+Improved handling of readonly path write-bit-setting.
diff --git a/news/33.bugfix.rst b/news/33.bugfix.rst
new file mode 100644
index 0000000..2d09f64
--- /dev/null
+++ b/news/33.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed a bug with encoding of output streams for dummy spinner and formatting exceptions.
diff --git a/setup.cfg b/setup.cfg
index 5a388b2..fdd12e0 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -59,6 +59,9 @@ strict = true
 addopts = -ra
 testpaths = tests/
 norecursedirs = .* build dist news tasks docs
+filterwarnings =
+    ignore::DeprecationWarning
+    ignore::PendingDeprecationWarning
 
 [isort]
 atomic=true
diff --git a/src/vistir/__init__.py b/src/vistir/__init__.py
index d64bf8d..676a014 100644
--- a/src/vistir/__init__.py
+++ b/src/vistir/__init__.py
@@ -1,7 +1,12 @@
 # -*- coding=utf-8 -*-
 from __future__ import absolute_import, unicode_literals
 
-from .compat import NamedTemporaryFile, TemporaryDirectory, partialmethod, to_native_string
+from .compat import (
+    NamedTemporaryFile,
+    TemporaryDirectory,
+    partialmethod,
+    to_native_string,
+)
 from .contextmanagers import (
     atomic_open_for_write,
     cd,
@@ -10,12 +15,23 @@ from .contextmanagers import (
     temp_path,
     spinner,
 )
-from .misc import load_path, partialclass, run, shell_escape
+from .misc import (
+    load_path,
+    partialclass,
+    run,
+    shell_escape,
+    decode_for_output,
+    to_text,
+    to_bytes,
+    take,
+    chunked,
+    divide,
+)
 from .path import mkdir_p, rmtree, create_tracked_tempdir, create_tracked_tempfile
 from .spin import VistirSpinner, create_spinner
 
 
-__version__ = '0.2.3.dev0'
+__version__ = "0.2.3.dev0"
 
 
 __all__ = [
@@ -38,5 +54,11 @@ __all__ = [
     "create_spinner",
     "create_tracked_tempdir",
     "create_tracked_tempfile",
-    "to_native_string"
+    "to_native_string",
+    "decode_for_output",
+    "to_text",
+    "to_bytes",
+    "take",
+    "chunked",
+    "divide",
 ]
diff --git a/src/vistir/misc.py b/src/vistir/misc.py
index 7d400a4..c61151d 100644
--- a/src/vistir/misc.py
+++ b/src/vistir/misc.py
@@ -489,3 +489,38 @@ try:
     locale_encoding = locale.getdefaultencoding()[1] or "ascii"
 except Exception:
     locale_encoding = "ascii"
+
+
+def getpreferredencoding():
+    """Determine the proper output encoding for terminal rendering"""
+
+    import locale
+    # Borrowed from Invoke
+    # (see https://github.com/pyinvoke/invoke/blob/93af29d/invoke/runners.py#L881)
+    _encoding = locale.getpreferredencoding(False)
+    if six.PY2 and not sys.platform == "win32":
+        _default_encoding = locale.getdefaultlocale()[1]
+        if _default_encoding is not None:
+            _encoding = _default_encoding
+    return _encoding
+
+
+PREFERRED_ENCODING = getpreferredencoding()
+
+
+def decode_for_output(output):
+    """Given a string, decode it for output to a terminal
+
+    :param str output: A string to print to a terminal
+    :return: A re-encoded string using the preferred encoding
+    :rtype: str
+    """
+
+    if not isinstance(output, six.string_types):
+        return output
+    try:
+        output = output.encode(PREFERRED_ENCODING)
+    except AttributeError:
+        pass
+    output = output.decode(PREFERRED_ENCODING)
+    return output
diff --git a/src/vistir/path.py b/src/vistir/path.py
index 68e6d46..ad0a8f2 100644
--- a/src/vistir/path.py
+++ b/src/vistir/path.py
@@ -8,6 +8,7 @@ import os
 import posixpath
 import shutil
 import stat
+import sys
 import warnings
 
 import six
@@ -166,11 +167,12 @@ def is_readonly_path(fn):
 
     Permissions check is `bool(path.stat & stat.S_IREAD)` or `not os.access(path, os.W_OK)`
     """
-    from .misc import to_bytes
+    from .compat import to_native_string
 
-    fn = to_bytes(fn, encoding="utf-8")
+    fn = to_native_string(fn)
     if os.path.exists(fn):
-        return bool(os.stat(fn).st_mode & stat.S_IREAD) and not os.access(fn, os.W_OK)
+        file_stat = os.stat(fn).st_mode
+        return not bool(file_stat & stat.S_IWRITE) or not os.access(fn, os.W_OK)
     return False
 
 
@@ -242,7 +244,7 @@ def create_tracked_tempdir(*args, **kwargs):
     tempdir = TemporaryDirectory(*args, **kwargs)
     TRACKED_TEMPORARY_DIRECTORIES.append(tempdir)
     atexit.register(tempdir.cleanup)
-    warnings.simplefilter("default", ResourceWarning)
+    warnings.simplefilter("ignore", ResourceWarning)
     return tempdir.name
 
 
@@ -266,12 +268,20 @@ def set_write_bit(fn):
     :param str fn: The target filename or path
     """
 
-    from .misc import to_bytes, locale_encoding
+    from .compat import to_native_string
 
-    fn = to_bytes(fn, encoding=locale_encoding)
+    fn = to_native_string(fn)
     if not os.path.exists(fn):
         return
-    os.chmod(fn, stat.S_IWRITE | stat.S_IWUSR | stat.S_IRUSR)
+    file_stat = os.stat(fn).st_mode
+    os.chmod(fn, file_stat | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+    if not os.path.isdir(fn):
+        return
+    for root, dirs, files in os.walk(fn, topdown=False):
+        for dir_ in [os.path.join(root,d) for d in dirs]:
+            set_write_bit(dir_)
+        for file_ in [os.path.join(root, f) for f in files]:
+            set_write_bit(file_)
 
 
 def rmtree(directory, ignore_errors=False):
@@ -288,14 +298,14 @@ def rmtree(directory, ignore_errors=False):
        Setting `ignore_errors=True` may cause this to silently fail to delete the path
     """
 
-    from .misc import locale_encoding, to_bytes
+    from .compat import to_native_string
 
-    directory = to_bytes(directory, encoding=locale_encoding)
+    directory = to_native_string(directory)
     try:
         shutil.rmtree(
             directory, ignore_errors=ignore_errors, onerror=handle_remove_readonly
         )
-    except (IOError, OSError) as exc:
+    except (IOError, OSError, FileNotFoundError) as exc:
         # Ignore removal failures where the file doesn't exist
         if exc.errno == errno.ENOENT:
             pass
@@ -316,23 +326,24 @@ def handle_remove_readonly(func, path, exc):
     :func:`set_write_bit` on the target path and try again.
     """
     # Check for read-only attribute
-    from .compat import ResourceWarning
-    from .misc import to_bytes
+    from .compat import ResourceWarning, FileNotFoundError, to_native_string
 
-    PERM_ERRORS = (errno.EACCES, errno.EPERM)
+    PERM_ERRORS = (errno.EACCES, errno.EPERM, errno.ENOENT)
     default_warning_message = (
         "Unable to remove file due to permissions restriction: {!r}"
     )
     # split the initial exception out into its type, exception, and traceback
     exc_type, exc_exception, exc_tb = exc
-    path = to_bytes(path, encoding="utf-8")
+    path = to_native_string(path)
     if is_readonly_path(path):
         # Apply write permission and call original function
         set_write_bit(path)
         try:
             func(path)
-        except (OSError, IOError) as e:
-            if e.errno in PERM_ERRORS:
+        except (OSError, IOError, FileNotFoundError) as e:
+            if e.errno == errno.ENOENT:
+                return
+            elif e.errno in PERM_ERRORS:
                 warnings.warn(default_warning_message.format(path), ResourceWarning)
                 return
 
@@ -340,17 +351,20 @@ def handle_remove_readonly(func, path, exc):
         set_write_bit(path)
         try:
             func(path)
-        except (OSError, IOError) as e:
+        except (OSError, IOError, FileNotFoundError) as e:
             if e.errno in PERM_ERRORS:
                 warnings.warn(default_warning_message.format(path), ResourceWarning)
+                pass
             elif e.errno == errno.ENOENT:  # File already gone
-                return
+                pass
             else:
                 raise
-            return
         else:
-            raise
-    raise exc
+            return
+    elif exc_exception.errno == errno.ENOENT:
+        pass
+    else:
+        raise exc_exception
 
 
 def walk_up(bottom):
diff --git a/src/vistir/spin.py b/src/vistir/spin.py
index 2a84892..20587d9 100644
--- a/src/vistir/spin.py
+++ b/src/vistir/spin.py
@@ -5,6 +5,7 @@ import os
 import signal
 import sys
 
+import colorama
 import cursor
 import six
 
@@ -31,7 +32,9 @@ CLEAR_LINE = chr(27) + "[K"
 
 class DummySpinner(object):
     def __init__(self, text="", **kwargs):
-        self.text = to_native_string(text)
+        colorama.init()
+        from .misc import decode_for_output
+        self.text = to_native_string(decode_for_output(text))
         self.stdout = kwargs.get("stdout", sys.stdout)
         self.stderr = kwargs.get("stderr", sys.stderr)
         self.out_buff = StringIO()
@@ -42,10 +45,11 @@ class DummySpinner(object):
         return self
 
     def __exit__(self, exc_type, exc_val, traceback):
-        if not exc_type:
-            self.ok(text=None)
-        else:
-            self.write_err(traceback)
+        if exc_type:
+            import traceback
+            from .misc import decode_for_output
+            self.write_err(decode_for_output(traceback.format_exception(*sys.exc_info())))
+        self._close_output_buffer()
         return False
 
     def __getattr__(self, k):
@@ -58,33 +62,42 @@ class DummySpinner(object):
         else:
             return retval
 
+    def _close_output_buffer(self):
+        if self.out_buff and not self.out_buff.closed:
+            try:
+                self.out_buff.close()
+            except Exception:
+                pass
+
     def fail(self, exitcode=1, text="FAIL"):
+        from .misc import decode_for_output
         if text and text != "None":
-            self.write_err(text)
-        if self.out_buff:
-            self.out_buff.close()
-        raise SystemExit(exitcode, text)
+            self.write_err(decode_for_output(text))
+        self._close_output_buffer()
 
     def ok(self, text="OK"):
         if text and text != "None":
             self.stderr.write(self.text)
-        if self.out_buff:
-            self.out_buff.close()
+        self._close_output_buffer()
         return 0
 
     def write(self, text=None):
+        from .misc import decode_for_output
         if text is None or isinstance(text, six.string_types) and text == "None":
             pass
-        self.stdout.write(to_native_string("\r"))
-        line = to_native_string("{0}\n".format(text))
+        text = decode_for_output(text)
+        self.stdout.write(decode_for_output("\r"))
+        line = decode_for_output("{0}\n".format(text))
         self.stdout.write(line)
         self.stdout.write(CLEAR_LINE)
 
     def write_err(self, text=None):
+        from .misc import decode_for_output
         if text is None or isinstance(text, six.string_types) and text == "None":
             pass
-        self.stderr.write(to_native_string("\r"))
-        line = to_native_string("{0}\n".format(text))
+        text = decode_for_output(text)
+        self.stderr.write(decode_for_output("\r"))
+        line = decode_for_output("{0}\n".format(text))
         self.stderr.write(line)
         self.stderr.write(CLEAR_LINE)
 
@@ -112,7 +125,6 @@ class VistirSpinner(base_obj):
         """
 
         self.handler = handler
-        import colorama
         colorama.init()
         sigmap = {}
         if handler:

Readonly path removals fail to set write bits in all cases
- Recursively crawl the directory tree when setting paths
- Make sure to set all permissions for all users on all paths
diff --git a/tests/test_path.py b/tests/test_path.py
index 4212fdb..69f3eb6 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -172,7 +172,7 @@ def test_handle_remove_readonly(tmpdir):
     test_file = tmpdir.join("test_file.txt")
     test_file.write_text("a bunch of text", encoding="utf-8")
     os.chmod(test_file.strpath, NON_WRITE_OR_EXEC)
-    fake_oserror = OSError("Fake os error")
-    fake_oserror.errno = 13  # EACCES error
-    vistir.path.handle_remove_readonly(os.unlink, test_file.strpath, (OSError, fake_oserror, "Fake Traceback"))
+    fake_oserror = OSError(13, "Permission denied")
+    fake_oserror.filename = test_file.strpath
+    vistir.path.handle_remove_readonly(os.unlink, test_file.strpath, (fake_oserror.__class__, fake_oserror, "Fake traceback"))
     assert not os.path.exists(test_file.strpath)

pip install -e .[spinner,tests]
parse_log_pytest
pytest
pytest
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
charset-normalizer==2.0.12
colorama==0.4.5
coverage==6.2
cursor==1.3.5
dataclasses==0.8
execnet==1.9.0
hypothesis==6.31.6
hypothesis-fspaths==0.1
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-cov==4.0.0
pytest-timeout==2.1.0
pytest-xdist==3.0.2
requests==2.27.1
six==1.17.0
sortedcontainers==2.4.0
termcolor==1.1.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
-e git+https://github.com/sarugaku/vistir.git@caca104f8f3aef05562cea1935bb3e0f0de4ed74#egg=vistir
yaspin==2.1.0
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: vistir
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==2.0.12
      - colorama==0.4.5
      - coverage==6.2
      - cursor==1.3.5
      - dataclasses==0.8
      - execnet==1.9.0
      - hypothesis==6.31.6
      - hypothesis-fspaths==0.1
      - idna==3.10
      - pytest-cov==4.0.0
      - pytest-timeout==2.1.0
      - pytest-xdist==3.0.2
      - requests==2.27.1
      - six==1.17.0
      - sortedcontainers==2.4.0
      - termcolor==1.1.0
      - tomli==1.2.3
      - urllib3==1.26.20
      - yaspin==2.1.0
prefix: /opt/conda/envs/vistir

tests/test_path.py::test_is_readonly_path tests/test_path.py::test_handle_remove_readonly
tests/test_path.py::test_abspathu tests/test_path.py::test_safe_expandvars tests/test_path.py::test_mkdir_p tests/test_path.py::test_ensure_mkdir_p tests/test_path.py::test_create_tracked_tempdir tests/test_path.py::test_rmtree tests/test_path.py::test_get_converted_relative_path tests/test_path.py::test_is_valid_url tests/test_path.py::test_path_to_url tests/test_path.py::test_normalize_drive tests/test_path.py::test_walk_up
README.rst docs/quickstart.rst src/vistir/misc.py src/vistir/path.py src/vistir/__init__.py news/33.bugfix.rst news/32.bugfix.rst setup.cfg src/vistir/spin.py
README.rst docs/quickstart.rst src/vistir/misc.py src/vistir/path.py src/vistir/__init__.py news/33.bugfix.rst news/32.bugfix.rst setup.cfg src/vistir/spin.py