A library for building modern declarative desktop applications in WX.

Related tags

GUI Developmentre-wx
Overview

A Python library for building modern declarative desktop applications


PyPI

Overview

re-wx is a library for building modern declarative desktop applications. It's built as a management layer on top of WXPython, which means you get all the goodness of a mature, native, cross-platform UI kit, wrapped up in a modern, React inspired API.

What is it?

It's a "virtualdom" for WX. You tell re-wx what you want to happen, and it'll do all the heavy lifting required to get WX to comply. It lets you focus on your state and business logic while leaving implentation details of WX's ancient API to re-wx.

Say goodbye to

  • Deep coupling of business logic to stateful widgets
  • Awkward auto-generated Python wrappers on old bloated C++ classes
  • Being forced to express UIs through low level A.GetLayout().addChild(B) style plumbing code

re-wx is:

  • Declarative
  • Component Based
  • 100% compatible with all WXPython code bases

Re-wx lets you build expressive, maintainable applications out of simple, testable, functions and components.

Alpha Note:

This is an early release and under active development. Expect a few bugs, feature gaps, and a bit of API instability. If you hit any snags, pop over to the issues and let me know!

Installation

The latest stable version is available on PyPi.

pip install re-wx 

Documentation

Quick Start: RE-WX in 5 minutes

re-wx has just a few core ideas: Elements, Components, and rendering. Everything else is achieved by combining these 3 ideas into larger and larger things.

All re-wx application consists of just a few steps.

  1. define your application view
  2. Rendering it to produce a wx object
  3. kick off the wx Main Loop.

Starting small: Hello World

import wx
from rewx import create_element, wsx, render     
from rewx.components import StaticText, Frame

if __name__ == '__main__':
    app = wx.App()    
    element = create_element(Frame, {'title': 'My Cool Application', 'show': True}, children=[
        create_element(StaticText, {'label': 'Howdy, cool person!'})
    ])
    frame = render(element, None)
    app.MainLoop()

Run this and you'll see the output on the right. While not glamorous yet, it lets us explore several of the main ideas.

At the heart of all re-wx applications is the humble Element. We used the function create_element to build them. Applications are built by composing trees of these elements together into larger and larger composite structures.

Here we've created two elements. A top-level Frame type, which is required by WXPython, and then an inner StaticText one, which displays text on the screen.

Elements all consist of three pieces of data: 1. the type of the entity we want to render into the UI, 3. the properties ("props" from here on out) we want that entity to have, and 3. any children, which are themselves Elements.

An important note is that Elements are plain data -- literally just a Python dict like this:

{
  'type': Frame, 
  'props': {
      'title': 'My Cool Application', 
      'show': True,
      'children': [{
        'type': StaticText,
        'props': {'label': 'Howdy, cool person!'}
      }]
  }

Together, these elements make up the "virtualdom" used by re-wx uses to drive the underlying WXWidgets components. Creating an element does not actually instantiate any WX elements. That job falls to render

rewx.render is how we transform our tree of Elements into a live UI. It handles all of the lifting required to instantiate the WX Objects, associate them all together, and put them in the state specified by your tree. The output of render is a WX Object, which in our example, is our top level frame.

With the frame now happily created, we just have to tell WXPython to start its main loop, which will launch the GUI, and we've officially built our first re-wx app!

A brief detour for WSX:

Writing all those create_element statements can get really tedious and creates a lot of visual noise which can make getting a feel for your UI's structure at a glance difficult. An alternative and recommended approach is to use wsx, which lets you use nested lists to express parent child relationships between components. It uses the exact same [type, props, *children] arguments as create_element, but with a terser more compact syntax. Here's the same example using wsx.

from rewx import wsx 
...
element = wsx(
  [Frame, {'title': 'My Cool Application', 'show': True}, 
    [StaticText, {'label': 'Howdy, cool person!'}]]
)

For the rest of this guide, we'll be using the wsx form, but you can use create_element if you prefer.


A Stateful component

Components are how you store and manage state in re-wx.

class Clock(Component):
    def __init__(self, props):
        super().__init__(props)
        self.timer = None
        self.state = {
            'time': datetime.datetime.now()
        }

    def component_did_mount(self):
        self.timer = wx.Timer()
        self.timer.Notify = self.update_clock
        self.timer.Start(milliseconds=1000)

    def update_clock(self):
        self.set_state({'time': datetime.datetime.now()})

    def render(self):
        return wsx(
          [c.Block, {},
           [c.StaticText, {'label': self.state['time'].strftime('%I:%M:%S'),
                           'name': 'ClockFace',
                           'foreground_color': '#51acebff',
                           'font': big_ol_font(),
                           'proporton': 1,
                           'flag': wx.CENTER | wx.ALL,
                           'border': 60}]]
        )
        
if __name__ == '__main__':
    app = wx.App()
    frame = wx.Frame(None, title='Clock')
    clock = render(create_element(Clock, {}), frame)
    frame.Show()
    app.MainLoop()        

Here we've setup a Component which keeps track of the current time and displays it nice and bold in the center of our frame.

There's lot going on here, so we'll take if from the top!

You define your own components by inheriting from rewx.Component. This gives you access to all the lifecycle and state management options provided by the base class. You can checkout the Main Concepts for the full details of the life cycle methods.

Components have a few notable methods:

Method Usage
__init__ This gets called when re-wx instantiates your class. This is where you specify your initial state. Note that this is called before the actual GUI elements are available. This method should be used only to initialize data, not deal with presentational concerns
render This is where you'll create your element tree which defines your UI.
component_did_mount This method is called once all of your Component's elements have been rendered and mounted onto a wx.Window. It's here that you can kick off any work which requires the GUI to be up and running
set_state This method is used update your components state and kick off a re-render of its visuals.

Still just an element

You use your component like any other Element we've encountered so far. Meaning, you don't instantiate it directly, you put in in your Element tree and let re-wx handle all the details.

That's what we're doing down at the bottom of the file where we wire the app together. We create an Element from our Component just like normal: create_element(Clock, {}) and pass it to our render function.

if __name__ == '__main__':
    app = wx.App()
    frame = wx.Frame(None, title='Clock')
    clock = render(create_element(Clock, {}), frame)
    frame.Show()
    app.MainLoop()  

An Application

Our final example will pull it all together. It combines plain Elements, Components, and business logic into a complete application.

def TodoList(props):
    return create_element(c.Block, {}, children=[
        create_element(c.StaticText, {'label': f" * {item}"})
        for item in props['items']
    ])


class TodoApp(Component):
    def __init__(self, props):
        super().__init__(props)
        self.state = {'items': ['Groceries', 'Laundry'], 'text': ''}

    def handle_change(self, event):
        self.set_state({**self.state, 'text': event.String})

    def handle_submit(self, event):
        self.set_state({
            'text': '',
            'items': [*self.state['items'], self.state['text']]
        })

    def render(self):
        return wsx(
            [c.Frame, {'title': 'My First TODO app'},
             [c.Block, {'name': 'main-content'},
              [c.StaticText, {'label': 'What needs to be done?'}],
              [c.TextCtrl, {'value': self.state['text']}],
              [c.Button, {'label': 'Add', 'on_click': self.handle_submit}],
              [c.StaticText, {'label': 'TO DO:'}],
              [TodoList, {'items': self.state['items'], 'on_click': self.handle_complete}]]]
        )

if __name__ == '__main__':
    app = wx.App()
    frame = render(create_element(TodoApp, {}), None)
    frame.Show()
    app.MainLoop()

Where to go from here?

Checkout the docs folder for more detailed guides and walk throughs

Philosophy

It's a library first. re-wx is "just" a library, not a framework. Beacuse it's a library, you can use as much or as little of as you need. It requires no application-level total buy in like a framwork would. You don't have to do everything the "re-wx way. Further, the output from a re-wx render is a plain old WXPython component. Meaning, all re-wx components ARE WX components, and thus require no special handling to integrate with your existing code base.

It's intended to be symbiotic with WXPython re-wx is not trying to be an general purpose abstraction over multiple backend UI kits. It's lofty goals begin and end with it being a way of making writing native, cross-platform UIs in WXPython easier. As such, it doesn't need reconcilers, or generic transactions, or any other abstraction related bloat. As a result, re-wx's core codebase is just a handful of files and can be understood in an afternoon.

Given the symbiotic nature, practicality is favored over purity of abstraction. You'll mix and match WXPython code and re-wx code as needed. A good example of this is for transient dialogs (confirming actions, getting user selectsions, etc..). In React land, you'd traditionally have a modal in your core markup, and then conditionally toggle its visibility via state. However, in re-wx, you'll just use the dialog directly rather than embedding it in the markup and handling its lifecycle via is_open style state flags. This is practical to do because, unlike React in Javascript, WX handles managing the UI thread thus allowing us to block in place without any negative effects. Which enables writing straight forward in-line Dialog code.

def handle_choose_dir(self, event): 
    dlg = wx.DirDialog(None)
    if dlg.Show() == wx.ID_OK:
        self.setState({'directory': dlg.GetPath()})

Compromises and caveats in the design

While you'll program in a declarative style and enjoy the benefits that one-way data flows bring, a caveat is that not all components technically follow the unidirectional dataflow. The design of WX and the native APIs means that certain events are only fired after internal states have been updated. So, for components like wx.ComboBox and wx.TextCtrl, handlers don't have a chance to operate until the widgets themseves have completed their work.

The good news is that in practice, this is generally something you'll never notice or need to worry about. All updates are all done inside of a Freeze/Thaw transaction, thus hiding any visual quirks or flicker which may have come from re-wx forcing WX back into the state you specify rather than its own internally managed one.

API Surface area:

Only the most common attributes are currently managed by declarative props (basically, most of what falls under wx.Control). For example, specifics such as InsertionPoints in TextCtrls are considered out of scope for rewx. Refs act as a handy escape-hatch for when you need access to the full WX API. Be sure to checkout the Componet Docs for the full list of supported props.

Stubborn Widgets:

Some WXPython widget, like the prefab RadioGroup, cannot have its number of options changed after creation. So, updating the choices prop will have no effect. Luckily, these components are few and far between, and usually have easy work arounds or alternatives. See the Componet Docs for more info.

Stuck? Need some help? Just have a question?

Open an issue here, or feel free to hit me up directly at [email protected] and we'll get it sorted out!

Contributing

All contributions are welcome! Just make sure you follow the Contributing Guidelines.

License

re-wx is MIT licensed.

Comments
  • Elm Architecture / Flux / Redux-style Global State Store

    Elm Architecture / Flux / Redux-style Global State Store

    Hello Chris,

    I found re-wx from a reply to my HackerNews comment at https://news.ycombinator.com/item?id=28328165.

    I'd like to build a re-wx app with a global state store and the Model-View-Update / The Elm Architecture / Flux / Redux pattern. Here's my use case:

    1.) Have a main thread running the GUI that users click on buttons and see readouts of the state in Gauges and StaticTexts 2.) Have several other threads running in the app that are doing background tasks. It's important to know that these background tasks aren't triggered from the user interacting with the UI except for the initial launch, which should spawn 3 threads that start doing stuff (downloading) in the background.

    As these tasks get completed, the background threads send messages using a Queue. An update function takes in the current state, a message from the queue, and produces a new global state. Ideally, that would cause the re-wx app to render and show updated messages in the Gauge and StaticText readouts of the download progress.

    Essentially a multithreaded downloader?

    Would you be able to share a toy script of how you'd implement a global state store and update that state store from outside the re-wx app but still cause the re-wx app to render?

    opened by z3ugma 8
  • write a FilePickerCtrl component

    write a FilePickerCtrl component

    I want to write a FilePickerCtrl component. Any advice?

    It seems like writing new primitive components is a bit awkward because the re.widgets.set_basic_props function has no way to amend the exclusions map. So I have to either fork re-wx or make a new set_basic_props function. Do I have that right?

    opened by jamesdbrock 4
  • Handle TextCtrl with wx.TE_PROCESS_ENTER

    Handle TextCtrl with wx.TE_PROCESS_ENTER

    Hi Chris - I'm attempting to define my own TextCtrl component that has style=wx.TE_PROCESS_ENTER and then Bind the wx.EVT_TEXT_ENTER event with a prop, something like on_enter for the TextCtrl. The Enter function would cause the gui to execute a function similar to how an on_click handler would. How would you recommend building such a component class?

    opened by z3ugma 2
  • Python and the Model-View-Update GUI Revolution

    Python and the Model-View-Update GUI Revolution

    My colleague @ramin-honary-xc wrote a blog post based on a presentation he and I did together about re-wx:

    https://xc-jp.github.io/blog-posts/2022/11/22/python-model-view-update-frameworks.html

    opened by jamesdbrock 1
  • Setting value of a gauge  with state does not update on first trigger of button

    Setting value of a gauge with state does not update on first trigger of button

    Hello!

    Here's a demo app I've put together. Could you help me answer 2 questions?

    1. How can I set the width and height of the Gauge element?
    2. If you run this app, you'll notice that the first time you click the Update button that the gauge value is not reflected in the GUI. When you click a second and subsequent times, it works as expected. How come?
    import wx
    from rewx import Component, wsx, render, create_element
    from rewx.components import Block, Button, Gauge
    
    
    class FooGauge(Component):
        def __init__(self, props):
            super().__init__(props)
            self.props = props
            self.state = {"counter": 1000}
    
        def update_count(self, event):
            self.set_state({"counter": self.state.get("counter", 0) + 100})
    
        def render(self):
            return wsx(
                [
                    Block,
                    {},
                    [
                        Gauge,
                        {
                            "value": self.state["counter"],
                            "range": 3000,
                            "name": "Counter",
                            "flag": wx.CENTER | wx.ALL,
                            "size": (500, 1),  # How to set size of this Gauge?
                            "border": 30,
                            "pulse": False,
                        },
                    ],
                    [
                        Button,
                        {
                            "label": "Update",
                            "on_click": self.update_count,
                            "flag": wx.CENTER | wx.ALL,
                        },
                    ],
                ],
            )
    
    
    if __name__ == "__main__":
        app = wx.App()
    
        # import wx.lib.inspection
        # wx.lib.inspection.InspectionTool().Show()
    
        frame = wx.Frame(None, title="Gauge With Update")
        clock = render(create_element(FooGauge, {}), frame)
    
        frame.Show()
        app.MainLoop()
    
    opened by z3ugma 1
  • Made a fork, cannot create PR from it

    Made a fork, cannot create PR from it

    Hello Chris, Looks like an awesome project! While reading the docs, I noticed some minor issues, so I've forked the repo and made some changes to the documentation (.md files), but I cannot push my branch to your repo (I get error 403).

    Please advise. Thanks, Amir

    opened by akrk1986 1
  • Add missing on_click handler for wx.StaticBitmap to comply with the docs

    Add missing on_click handler for wx.StaticBitmap to comply with the docs

    on_click was missing but is supposed to exist according to the docs https://github.com/chriskiehl/re-wx/blob/main/docs/supported-wx-components.md#StaticBitmap

    I also need the on_click handler on a bitmap in my project.

    opened by ronny-rentner 0
  • StaticBitmap tooltip does not display

    StaticBitmap tooltip does not display

    Love the project, coming from react land into wx world has never been easier!

    I'm having trouble getting the tooltip to show up on a StaticBitmap. Seems to work fine on Block and Button. I tested it alone and with other components, no luck getting it to display:

        element = wsx(
            [
                c.Frame,
                {"title": "Tooltip demo", "show": True},
                [
                    c.Block,
                    {"tooltip": "Testing tooltip block..."},
                    [c.Button, {"label": "Button", "tooltip": "Test button tooltip"}],
                    [c.StaticBitmap, {"uri": "help.png", "tooltip": "Test tooltip image"}],
                ],
            ]
    
    opened by bkVBC 0
  • Why so many Layout()s?

    Why so many Layout()s?

    https://github.com/chriskiehl/re-wx/blob/882967e5cbe14a2b65f45b5cfccea3cccdb293f4/rewx/core.py#L140-L143

    https://docs.wxpython.org/wx.Window.html#wx.Window.Layout

    Every time an element gets patched, Layout() is called on all the element’s transitive parents.

    So if an element has N transitive children, then Layout() will be called on the element N times during each render.

    Is that deliberate? Seems expensive? Why is that necessary?

    opened by jamesdbrock 3
  • Notes on core.py

    Notes on core.py

    self_managed seems like a useful attribute. It’s not mentioned anywhere else in the package, but this implementation seems reasonable and maybe it works?

    https://github.com/chriskiehl/re-wx/blob/882967e5cbe14a2b65f45b5cfccea3cccdb293f4/rewx/core.py#L86

    component_will_unmount is never called anywhere, so this feature is not yet implemented.

    https://github.com/chriskiehl/re-wx/blob/882967e5cbe14a2b65f45b5cfccea3cccdb293f4/rewx/core.py#L195

    This re-wx package seems like the best cross-platform native GUI Python package on the internet. Or anyway, the least insane. Everything else is either web-based (Streamlit), or 20th-century object-oriented trash (QT for Python), or looks so horrible that you can’t distribute it to any users. (DearPyGUI).

    But the Alpha Note on the README is not just false modesty. This package really is not yet reliable. Is there any possibility that you will resume your interest in this project @chriskiehl ? I would like to work on improvement to re-wx but my Python is bad and I don’t understand the code for re-wx as well as you. Reading the code, I feel like you had a roadmap for re-wx in your head, but you didn’t have time to work it all out. What are your thoughts on the future of this package?

    opened by jamesdbrock 6
  • ListCtrl set_state segfault

    ListCtrl set_state segfault

    When I have a ListCtrl in my tree and I call set_state, I get a segfault here:

    https://github.com/chriskiehl/re-wx/blob/882967e5cbe14a2b65f45b5cfccea3cccdb293f4/rewx/core.py#L135

    Fatal Python error: Segmentation fault
    
    Current thread 0x00007f82652a2740 (most recent call first):
      File "/home/jbrock/work/xc/JIT_GUI3/./submodule/re-wx/rewx/core.py", line 135 in patch
      File "/home/jbrock/work/xc/JIT_GUI3/./submodule/re-wx/rewx/core.py", line 104 in patch
      File "/home/jbrock/work/xc/JIT_GUI3/./submodule/re-wx/rewx/core.py", line 104 in patch
      File "/home/jbrock/work/xc/JIT_GUI3/./submodule/re-wx/rewx/core.py", line 212 in set_state
      File "/home/jbrock/work/xc/JIT_GUI3/jit.py", line 759 in handle_butten
      File "/nix/store/qzrv41n5svhjzq3006qxkc2aar2wzw08-python3-3.10.6-env/lib/python3.10/site-packages/wx/core.py", line 2262 in MainLoop
      File "/home/jbrock/work/xc/JIT_GUI3/jit.py", line 870 in <module>
    
    Extension modules: numpy.core._multiarray_umath, numpy.core._multiarray_tests, numpy.linalg._umath_linalg, numpy.fft._pocketfft_internal, numpy.random._common, numpy.random.bit_generator, numpy.random._bounded_integers, numpy.random._mt19937, numpy.random.mtrand, numpy.random._philox, numpy.random._pcg64, numpy.random._sfc64, numpy.random._generator, cv2, torch._C, torch._C._fft, torch._C._linalg, torch._C._nn, torch._C._sparse, torch._C._special, wx._core, yaml._yaml, wx._adv, wx._media, wx._html, wx._html2, wx.svg._nanosvg, wx._xml, wx._richtext, PIL._imaging, wx._stc (total: 31)
    Segmentation fault (core dumped)
    

    I'm investigating.

    opened by jamesdbrock 2
  • Assorted improvements

    Assorted improvements

    I'm working on a FilePickerCtrl. Do you want a PR for this?

    Resolves #11


    Update

    This has turned into an omni-PR for a lot of little improvements that I had to make in order use re-wx for my (private, proprietary) project. In the end it all worked out and the project was finished and deployed.

    Widgets

    New components:

    • FlexGrid
    • FilePickerCtrlOpen
    • FilePickerCtrlSave
    • DirPickerCtrl

    Improved components:

    • ScrolledPanel
    • Button
    • ComboBox
    • ListCtrl
    • SpinCtrl
    • SpinCtrlDouble
    • Block

    Core

    • Empty child lists are allowed.
    • The patch reconciliation can now correctly handle the case where the type of a child changes. (This is great news because this is where the real magic of a declarative TEA GUI really happens. This is why re-wx is a generation better than horrible static design domain-specific languages ). We still don’t have 'key' props for performance.
    • Layout() each Sizer once for each patch. Resolves #15
    opened by jamesdbrock 1
Releases(0.0.08)
Owner
Chris
Full stack developer and general awesome person
Chris
Web-Broswer simple using PyQt5 tools

Web-Broswer Simple web broswer made using PyQt Completely simple and easy to use How to set it up git clone https://github.com/AsjadOooO/Web-Broswer.g

Asjad 3 Nov 13, 2021
This is a short GUI project to evaluate Pbk solution.

Polubarinova-Kochina-solutions (Standalone GUI executables for Windows and Mac) Oden Institute for Computational Engineering and Sciences / Jackson Sc

Mohammad Afzal Shadab 1 Dec 24, 2022
Advanced GUI Calculator with Beautiful UI and Clear Code.

Advanced GUI Calculator with Beautiful UI and Clear Code.

Mohammad Dori 3 Jul 15, 2022
A GUI frontend for the Kamyroll-API using Python and PySide6

Kamyroll-GUI A GUI frontend for the Kamyroll-API using Python and PySide6 Usage When starting the application you will be presented with a list and so

Simon Sawicki 15 Oct 09, 2022
Create shortcuts on Windows to your Python, EXE, Batch files or any other file using a GUI made with PySimpleGUI

shor Windows Shortcut Creation Create Windows Shortcuts to your python programs and any other file easily using this application created using PySimpl

PySimpleGUI 7 Nov 16, 2021
PyQT5 app for LOLBAS and GTFOBins

LOLBins PyQT app to list all Living Off The Land Binaries and Scripts for Windows from LOLBAS and Unix binaries that can be used to bypass local secur

Hamza Megahed 41 Dec 01, 2022
Firefox 96 Webapps for Gnome 3

mozapp Do you prefer Firefox to Chrome? Me too! But ever since Firefox dropped support for standalone web applications, I've resorted to using Chrome

Marten de Vries 8 Oct 31, 2022
`rosbag filter` with Gooey-based GUI

rosbag_filter_gui rosbag filter with Gooey-based GUI Test-passed Ubuntu 20.04 ROS Noetic Python 3.8 Installation

Yujie He 2 Dec 07, 2021
Bill Cipher is a Python3 Tkinter Application that creates Python remote backdoors, while giving you the option to convert it to an exe.

Bill Cipher is a Python3 Tkinter Application that creates Python remote backdoors, while giving you the option to convert it to an exe. The program also configures a .py server file that works with t

Damian Mostert 2 Apr 12, 2022
Easily display all of your creative avatars to keep them consistent across websites.

PyAvatar Easily display all of your creative avatars to keep them consistent across websites. Key Features • Download • How To Use • Support • Contrib

William 2 Oct 02, 2022
A GUI based CRUD database management system built using mysql and python

A GUI based CRUD database management system built using mysql and python

Aquila 2 Feb 13, 2022
Plantasia, all your plants and muchrooms in one place!

Plantasia Project Description Tkinter GUI to be used as a repository for plants and muchrooms. It helps to optimize the search for species that have h

Marco Rodrigues 1 Dec 23, 2021
Redis GUI using Qt & Python

QRedis A Python, Qt based Redis client user interface. Help wanted Open to people who want to colaborate. Would like to know which features you would

Tiago Coutinho 58 Dec 09, 2022
Windows & Linux GUI application to use a Satodime (satodime.io)

Satodime-Tool Licence: LGPL v3 Author: Toporin Language: Python (= 3.6) Homepage: https://github.com/Toporin/Satodime-Tool Introduction What is Satod

4 Dec 16, 2022
Dress up your code with a beautiful graphical user interface !

Dresscode Dress up your code with a beautiful graphical user interface ! This project is part of the Pyrustic Ecosystem. Look powered by the cyberpunk

20 Aug 24, 2022
Learn to build a Python Desktop GUI app using pywebview, Python, JavaScript, HTML, & CSS.

Python Desktop App Learn how to make a desktop GUI application using Python, JavaScript, HTML, & CSS all thanks to pywebview. pywebview is essentially

Coding For Entrepreneurs 55 Jan 05, 2023
Python Screen Recorder using Python

PY-Screen-Recorder Python Screen Recorder using Python Requirement: pip install cv2 pip install pyautogui pip install numpy How to reach me? You can r

SonLyte 8 Nov 08, 2021
A simple GUI designer for the python tkinter module

Leer en Español Welcome to Pygubu! Pygubu is a RAD tool to enable quick and easy development of user interfaces for the Python's tkinter module. The u

Alejandro Autalán 1.7k Dec 27, 2022
Create custom desktop notificatons using python

Create custom desktop notificatons using python In this video i am going to use a module called plyer

Niranjan 2 Dec 15, 2021
A system tray application written in python that will assist you with your keyboard endeavors.

A system tray application written in python that will assist you with your keyboard endeavors. It has features such as abbreviation, email autofill, media control, writing from clipboard ,typing curr

Mach50 1 Dec 15, 2021