-
Notifications
You must be signed in to change notification settings - Fork 32
Merge shell into master to add Shell widget #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| Table | ||
| ===== | ||
|
|
||
| .. currentmodule:: ttkwidgets | ||
|
|
||
| .. autoclass:: Shell | ||
| :show-inheritance: | ||
| :members: | ||
|
|
||
| .. automethod:: __init__ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import os | ||
| import tkinter as tk | ||
|
|
||
| from ttkwidgets import Shell | ||
|
|
||
|
|
||
| def onreturn(buffer): | ||
| import shlex | ||
| lexed = shlex.split(buffer, posix=True) | ||
| shell.print(lexed) | ||
|
|
||
| def contractuser(path): | ||
| expand = os.path.expanduser('~') | ||
| return path.replace(expand, '~') | ||
|
|
||
| root = tk.Tk() | ||
| root.title(os.getcwd()) | ||
| shell = Shell(root, prefix=contractuser(os.getcwd()) + ' ') | ||
| shell.add_command('onreturn', onreturn) | ||
| shell.pack() | ||
| root.mainloop() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Copyright (c) Dogeek 2020 | ||
| # For license see LICENSE | ||
| from ttkwidgets import Shell | ||
| from tests import BaseWidgetTest | ||
|
|
||
|
|
||
| class TestShell(BaseWidgetTest): | ||
| def test_shell_init(self): | ||
| shell = Shell(self.window) | ||
| shell.pack() | ||
| self.window.update() | ||
| shell.destroy() | ||
|
|
||
| def test_shell_forcefocus(self): | ||
| shell = Shell(self.window, force_focus=True) | ||
| shell.after(1000, lambda: self.assertIsNotNone(shell.focus_get())) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| import tkinter as tk | ||
| import tkinter.font as tkfont | ||
| from collections import defaultdict | ||
|
|
||
|
|
||
| class Shell(tk.Canvas): | ||
| def __init__(self, master, textvariable=None, prefix='', force_focus=True, | ||
| font=None, **kwargs): | ||
| """ | ||
| :param master: parent widget | ||
| :type master: tkinter.Widget | ||
| :param textvariable: A tkinter variable that holds the current text buffer | ||
| :type textvariable: tkinter.StringVar or None | ||
| :param prefix: A prefix to show on every input line | ||
| :type prefix: str | ||
| :param force_focus: whether or not the shell should take the focus. | ||
| :type force_focus: bool | ||
| :param font: Font to use for the terminal | ||
| :type font: tkinter.font.Font, tuple, or None | ||
| """ | ||
| for key, value in { | ||
| 'background': 'black', | ||
| 'takefocus': True, | ||
| }.items(): | ||
| if key not in kwargs: | ||
| kwargs[key] = value | ||
| super().__init__(master, **kwargs) | ||
| if kwargs['takefocus'] and force_focus: | ||
| self.focus_force() | ||
|
|
||
| self.bind('<Key>', self.on_key_press) | ||
| self.bind('<Configure>', self.on_configure) | ||
| self.textvariable = textvariable if isinstance(textvariable, tk.StringVar) else tk.StringVar() | ||
| self.prefix = prefix | ||
| self.font = font or ('Terminal', 10) | ||
| self.line_pos = (5, 5) | ||
| self.texts = [] | ||
| self.last_text = None | ||
| self.cursor = None | ||
| self.blink = True | ||
| self.buffer = prefix | ||
| self.text_update() | ||
| self.commands = defaultdict(list) | ||
| self.cursor_blink() | ||
|
|
||
| def on_key_press(self, event): | ||
| if event.keysym == 'Return' and len(self.buffer) > len(self.prefix): | ||
| self.texts.append(self.last_text) | ||
| span = self.text_line_span() | ||
| self.last_text = None | ||
| self.line_pos = (5, self.line_pos[1] + 15 * span) | ||
| self.call_command('onreturn', self.buffer[len(self.prefix):]) | ||
| self.buffer = self.prefix | ||
| self.text_update() | ||
| return | ||
|
|
||
| if event.keysym == 'Tab': | ||
| self.call_command('ontab', self) | ||
| return | ||
|
|
||
| if event.keysym == 'BackSpace' and len(self.buffer) > len(self.prefix): | ||
| self.buffer = self.buffer[:-1] | ||
| self.text_update() | ||
| return | ||
|
|
||
| if event.char.strip() or event.keysym == 'space': | ||
| self.buffer += event.char | ||
| self.text_update() | ||
| return | ||
|
|
||
| def on_configure(self, event): | ||
| padding = 4 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Ubuntu 20.04, GNOME, Python 3.8.2, a padding value of 4 makes the widget disappear until manually resized. A padding size of 2 solves this issue. I understand that this is here for stretching the widget to the size of its master, but, speaking from personal experience, this introduces head-aches like this when working with Canvases.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On windows, a padding of 2 makes the widget grow indefinitely |
||
| width = self.master.winfo_width() - padding | ||
| height = self.master.winfo_height() - padding | ||
| self.config(width=width, height=height) | ||
| for t in self.texts: | ||
| self.itemconfig(t, width=width) | ||
| self.itemconfig(self.last_text, width=width) | ||
|
|
||
| def text_update(self): | ||
| """ | ||
| Updates the text on the screen. | ||
|
sbordeyne marked this conversation as resolved.
Outdated
|
||
| """ | ||
| self.textvariable.set(self.buffer) | ||
| if self.last_text: | ||
| self.delete(self.last_text) | ||
| kwargs = { | ||
| 'anchor': tk.NW, | ||
| 'fill': 'white', | ||
| 'text': self.buffer, | ||
| 'width': self['width'], | ||
| 'font': self.font, | ||
| } | ||
| self.last_text = self.create_text(*self.line_pos, **kwargs) | ||
| return | ||
|
sbordeyne marked this conversation as resolved.
Outdated
|
||
|
|
||
| def cursor_blink(self): | ||
|
sbordeyne marked this conversation as resolved.
Outdated
|
||
| if self.cursor: | ||
| self.delete(self.cursor) | ||
| self.cursor = None | ||
| if self.last_text is not None and self.blink: | ||
| pos = self.cursor_pos | ||
| font = self.itemcget(self.last_text, 'font').split() | ||
| width, height = self.max_char_width, int(font[-1]) | ||
| pos = pos + (pos[0] + width, pos[1] + height) | ||
| self.cursor = self.create_rectangle(*pos, fill='white') | ||
| self.blink = not self.blink | ||
| self.after(1000, self.cursor_blink) | ||
|
|
||
| @property | ||
| def max_char_width(self): | ||
| """ | ||
| Gets the width of a W character | ||
|
|
||
| :returns: width of W with the font the shell uses | ||
| :rtype: int | ||
| """ | ||
| font = tkfont.Font(self, font=self.itemcget(self.last_text, 'font')) | ||
| return font.measure('W') | ||
|
|
||
| @property | ||
| def cursor_pos(self): | ||
| text = self.itemcget(self.last_text, 'text') | ||
| font = tkfont.Font(self, font=self.itemcget(self.last_text, 'font')) | ||
| text_len = font.measure(text) | ||
| width = self.max_char_width | ||
| span = self.text_line_span() | ||
| x = text_len % int(self['width']) + width + self.line_pos[0] | ||
| y = self.line_pos[1] + 15 * (span - 1) | ||
| return x, y | ||
|
|
||
| def text_line_span(self): | ||
|
sbordeyne marked this conversation as resolved.
|
||
| """ | ||
| Gets the number of lines to display the current text | ||
|
|
||
| :returns: number of lines | ||
| :rtype: int | ||
| """ | ||
| text = self.itemcget(self.last_text, 'text') | ||
| font = tkfont.Font(self, font=self.itemcget(self.last_text, 'font')) | ||
| text_len = font.measure(text) | ||
| n_lines = text_len // int(self['width']) + 1 | ||
| return n_lines | ||
|
|
||
| def call_command(self, command, *args): | ||
| """ | ||
| Calls command callbacks | ||
|
|
||
| :param command: Command name to call | ||
| :type command: str | ||
| """ | ||
| commands = self.commands.get(command, []) | ||
| if callable(commands): | ||
| return commands(*args) | ||
|
|
||
| for callback in commands: | ||
| callback(*args) | ||
|
|
||
|
sbordeyne marked this conversation as resolved.
|
||
| def add_command(self, command, *callbacks): | ||
| """ | ||
| Adds a command callback | ||
|
|
||
| :param command: command name to bind the callback to | ||
| :type command: str | ||
| :param *callbacks: list of callbacks to bind to the command | ||
| :type *callbacks: list[callable] | ||
| """ | ||
| assert all(callable(callback) for callback in callbacks), f'Callback should be a function' | ||
| self.commands[command].extend(callbacks) | ||
|
|
||
| def print(self, message): | ||
|
sbordeyne marked this conversation as resolved.
Outdated
|
||
| """ | ||
| Prints a message on the screen | ||
|
|
||
| :param message: message to write | ||
| :type message: str | ||
| """ | ||
| self.buffer = message | ||
| self.text_update() | ||
| self.texts.append(self.last_text) | ||
| span = self.text_line_span() | ||
| self.last_text = None | ||
| self.line_pos = (5, self.line_pos[1] + 15 * span) | ||
| self.buffer = self.prefix | ||
| self.text_update() | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The example has been moved to a separate file so may be removed here. |
||
| import os | ||
|
|
||
| def onreturn(buffer): | ||
| import shlex | ||
| lexed = shlex.split(buffer, posix=True) | ||
| print(lexed) | ||
|
|
||
| def contractuser(path): | ||
| expand = os.path.expanduser('~') | ||
| return path.replace(expand, '~') | ||
|
|
||
| root = tk.Tk() | ||
| root.title(os.getcwd()) | ||
| shell = Shell(root, prefix=contractuser(os.getcwd()) + ' ') | ||
| shell.add_command('onreturn', onreturn) | ||
| shell.pack() | ||
| root.mainloop() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quite some time ago I started implementing a similar widget on the
Consolebranch. Personally, I would add quite some features, including pressing up and down to cycle through command history, using thetabto have some form of completion...The
textvariableis modifiable by the creator of the widget, which inherently allows the creator to implement all these things. It is, however, not easy to do things with\bor other special characters. Perhaps it is worth considering to implement these things in the base widget.