├── .gitignore ├── pics ├── picture.jpeg ├── settings.png └── terminal-app.png ├── Makefile ├── start.py └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /*.png 2 | /ctrl-space.bin 3 | !/terminal-app.png 4 | /venv 5 | -------------------------------------------------------------------------------- /pics/picture.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex028502/extra-screen/HEAD/pics/picture.jpeg -------------------------------------------------------------------------------- /pics/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex028502/extra-screen/HEAD/pics/settings.png -------------------------------------------------------------------------------- /pics/terminal-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex028502/extra-screen/HEAD/pics/terminal-app.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | original=1280px-DEC_VT100_terminal_transparent.png 3 | 4 | all: picture.png icon.png ctrl-space.bin 5 | ctrl-space.bin: Makefile 6 | echo -ne '\x00' > $@ 7 | picture.png: $(original) 8 | convert $< -resize 96x96 $@ 9 | icon.png: $(original) 10 | convert $< -resize 48x48 $@ 11 | $(original): Makefile 12 | rm -f $@ 13 | wget https://upload.wikimedia.org/wikipedia/commons/thumb/9/9f/DEC_VT100_terminal_transparent.png/$@ 14 | touch $@ 15 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import os 4 | import tempfile 5 | 6 | import gi 7 | 8 | gi.require_version("Gtk", "3.0") 9 | from gi.repository import Gtk, Gdk, GdkPixbuf, Gio 10 | 11 | __dir__ = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | def debug(*args): 15 | if os.isatty(0): 16 | print(*args) 17 | 18 | 19 | session_name = sys.argv[1] 20 | 21 | # These are the only special cases I have needed so far, but you will surely 22 | # discover others that you need. I made these the same as what the terminal 23 | # emulator produces. However, I think there is no reason you couldn't just 24 | # start your screen session with whichever TERM you want, and make these match 25 | # since we are not using the tablet keyboard. For example, you could start 26 | # screen with TERM=ansi and then send ^H instead of ^? for backspace. I think 27 | # it will work as long as screen is started with the TERM variable that matches 28 | # the values in this table, because I think (hope) that screen will then 29 | # translate all incoming signals to the TERM=screen from whatever you language 30 | # you told it you were going to speak to it. 31 | 32 | SPECIAL_CASES = { 33 | "Up": "\x1b[A", 34 | "Down": "\x1b[B", 35 | "Right": "\x1b[C", 36 | "Left": "\x1b[D", 37 | "Home": "\x1b[H", 38 | "End": "\x1b[F", 39 | "Page_Up": "\x1b[5~", 40 | "Page_Down": "\x1b[6~", 41 | "BackSpace": "\x7f", 42 | "Delete": "\x1b\x5b\x33\x7e", 43 | } 44 | 45 | 46 | # Some terminal emulators send ascii 'delete' for the backspace key and others 47 | # send ascii 'backspace' for the backspace key. None of them send ascii 48 | # `backspace' or ascii 'delete' for the 'delete' key. 49 | 50 | 51 | def interpret(event): 52 | # lots of named key events have a pretty simple relationship with key codes 53 | if Gdk.keyval_name(event.keyval) in SPECIAL_CASES: 54 | return SPECIAL_CASES[Gdk.keyval_name(event.keyval)] 55 | 56 | # When you hold down Alt, the terminal emulator just sticks escape in front 57 | # of everything. 58 | if event.state & Gdk.ModifierType.MOD1_MASK: 59 | return "\x1b" + event.string 60 | 61 | if event.string == "\\": 62 | return "\\\\" 63 | 64 | if event.string == "^": 65 | return "\\^" 66 | 67 | return event.string 68 | 69 | 70 | def on_drag_data_received(widget, drag_context, x, y, data, info, time): 71 | uris = data.get_uris() 72 | if uris: 73 | for uri in uris: 74 | debug("uri %s" % uri) 75 | file = Gio.File.new_for_uri(uri) 76 | file_path = file.get_path() 77 | debug("file path: " + file_path) 78 | subprocess.run(["screen", "-S", session_name, "-X", "stuff", file_path]) 79 | 80 | 81 | def key_press_event(widget, event): 82 | key = Gdk.keyval_name(event.keyval) 83 | debug("---------") 84 | debug("pressed %s (%s)" % (key, len(event.string))) 85 | 86 | message = interpret(event) 87 | 88 | debug("message len", len(message)) 89 | debug("string len", len(event.string)) 90 | for i in event.string: 91 | debug("char: %s" % ord(i)) 92 | for attr in dir(event): 93 | if not attr.startswith("__"): 94 | debug(f"{attr}: {getattr(event, attr)}") 95 | 96 | if key == "V" and (event.state & Gdk.ModifierType.CONTROL_MASK): 97 | debug("paste!!!") 98 | clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 99 | clip_text = clipboard.wait_for_text() 100 | if clip_text: 101 | # from man screen 102 | # > You cannot paste large buffers with the stuff command. It is 103 | # most useful for key bindings. 104 | # but actually it worked pretty well - the problem that made me 105 | # stop using stuff here is that screen seems to expand whatever you 106 | # give it as if it were in single quotes and I couldn't figure out 107 | # a way to escape it to exactly that level 108 | with tempfile.NamedTemporaryFile(delete=False) as tmp_file: 109 | tmp_file.write(clip_text.encode()) 110 | subprocess.run( 111 | ["screen", "-S", session_name, "-X", "readbuf", tmp_file.name] 112 | ) 113 | subprocess.run(["screen", "-S", session_name, "-X", "paste", "."]) 114 | os.unlink(tmp_file.name) 115 | return 116 | 117 | # this case is so special it doesn't even fit into special cases 118 | # '\x00' doesn't work the other way because of the 'embedded null byte' 119 | # this is a bit clunky and puts a message about slurping in the terminal 120 | # screen, but it is just for copying the odd thing with tmux (or emacs) 121 | # so it works OK 122 | if event.keyval == Gdk.KEY_space and (event.state & Gdk.ModifierType.CONTROL_MASK): 123 | debug("special event") 124 | subprocess.run( 125 | ["screen", "-S", session_name, "-X", "readbuf", __dir__ + "/ctrl-space.bin"] 126 | ) 127 | subprocess.run(["screen", "-S", session_name, "-X", "paste", "."]) 128 | return 129 | 130 | subprocess.run( 131 | ["screen", "-S", session_name, "-X", "stuff", message], 132 | ) 133 | 134 | 135 | win = Gtk.Window() 136 | win.connect("destroy", Gtk.main_quit) 137 | image_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale( 138 | "./picture.png", 139 | 96, 140 | 96, 141 | True, 142 | ) 143 | image = Gtk.Image() 144 | image.set_from_pixbuf(image_pixbuf) 145 | win.add(image) 146 | 147 | win.set_title("\U0001F4BB") 148 | win.connect("key-press-event", key_press_event) 149 | win.set_icon_from_file("./icon.png") 150 | win.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY) 151 | win.drag_dest_add_uri_targets() 152 | win.connect("drag-data-received", on_drag_data_received) 153 | win.show_all() 154 | Gtk.main() 155 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Use an old tablet as an extra monitor 2 | 3 | (as long as you want to use it as a terminal) 4 | 5 | ![Kindle Fire as Extra Screen](pics/picture.jpeg) 6 | 7 | I have a couple old kindle fire tablets lying around. One of them has a battery 8 | that lasts about ten minutes. I also never have enough screens and never know 9 | where to put my terminal when I need to tail a log or something. 10 | 11 | I tried using [Deskreen](https://github.com/pavlobu/deskreen) a long time ago 12 | as my optional extra screen when I travel. I can't remember why I didn't get 13 | that habit going. 14 | 15 | For my extra office screen, I decided to do something different because 16 | * My graphics card seems to be maxed out. I can't even use the internal screen. 17 | * I don't know where I put that virtual display adapter. 18 | * It seemed like a bit of a detour to send pixels to my tablet when I am only 19 | looking at text 20 | 21 | #### So what is then? 22 | 23 | Well I just ssh into my computer from an android SSH client, open `screen`, and 24 | then use a program I made that allows me to `stuff` the characters I type into 25 | the screen session, so it feels like I am typing right into the tablet. 26 | 27 | ![typing app](pics/terminal-app.png) 28 | 29 | See if you can find this little window in the main picture. That is what I have 30 | to activate using Alt+Tab to type into the terminal. 31 | 32 | #### This is an 'MVP' and a 'POC' and stuff like that 33 | 34 | While I think this approach to using a spare tablet as an extra screen may be 35 | a winner, I am not so sure about this approach to this approach to using a 36 | spare tablet as an extra screen. Here are the issues: 37 | * You have to give some a mobile app access to your entire computer. This seems 38 | like a shame after the phone OS did all this work to sandbox the mobile app, and 39 | not give it access to your phone or tablet. This is even less awesome for an 40 | iPad since there don't seem to be any open source SSH clients, and even if 41 | there were, I don't think there would be any guarantee that what you see on 42 | github is what has been sent to the app store. 43 | * You have to open an SSH server on your computer that exposes you to some 44 | clever hacker in the Starbucks where you are using your spare screen 45 | * I can't figure out how to stuff C-SPC, which is a 0x0 - so have a clunky 46 | workaround 47 | * It seems weird to execute `screen` once per key. I wish I could just pipe the 48 | characters in or something - not that I have any idea how relevant that is to 49 | performance. 50 | * It's hard to find terminal clients for some old devices (like my iPad) 51 | 52 | What I think might be a better approach to this approach is using a lot of the 53 | bits and pieces in [hyper](https://hyper.is/), such as 54 | [xterm.js](http://xtermjs.org/), showing the terminal in a web browser, and 55 | sending the characters over a web socket. It would still only send just over a 56 | byte per keypress or something like that, and not use your GPU/HDMI, but could 57 | work on even more devices - any device with a browser, just like Deskreen. The 58 | server app would have to do much more: 59 | * serve the client app 60 | * make you type in a four digita code that you see on your tablet screen sort of 61 | like Bluetooth pairing 62 | * set up the shell, and PTY probably 63 | * listen to the console end of the PTY and send everything down the websocket 64 | * send key presses to the PTY 65 | 66 | Both solutions depend on the LAN, which is too bad. Maybe Web Bluetooth or 67 | something for future old devices. 68 | 69 | ## Set-up 70 | 71 | I don't really think anybody else should use my Python program that stuffs keys 72 | into the screen session (unless it's an emergency). You are better off taking 73 | the idea and making your own even better solution. But I'll first explain it 74 | using my program. 75 | 76 | ##### enable ssh on your computer 77 | 78 | ``` 79 | sudo apt-get install openssh-server 80 | ``` 81 | 82 | but make sure it is disabled so that you don't have it on unnecessarily, or in 83 | public places. 84 | 85 | ``` 86 | sudo systemctl disable ssh 87 | ``` 88 | 89 | start it whenever you are playing with your extra screen and on a trusted 90 | network: 91 | 92 | ``` 93 | sudo systemctl start ssh 94 | ``` 95 | 96 | disable it the rest of the time 97 | 98 | ``` 99 | sudo systemctl stop ssh 100 | ``` 101 | 102 | ##### open a terminal/ssh client on your tablet 103 | 104 | I side-loaded ConnectBot onto my Kindle Fire from F-Droid. I tried to sideload 105 | F-Droid, and then install ConnectBot, but it failed for some reason 106 | 107 | ##### start screen with a known session name: 108 | 109 | ``` 110 | DISPLAY=:0 screen -R aux 111 | ``` 112 | 113 | If you are using ConnectBot on a Kindle Fire, there seems to be a 114 | [known bug](https://github.com/connectbot/connectbot/issues/543) that return 115 | doesn't work. There are some workarounds for emergencies in the bug report but 116 | I just configured the above to happen every time I connected. 117 | 118 | `DISPLAY=:0` makes it so that you can do stuff like `xdg-open .` and `emacs &` 119 | and `git gui &` in the terminal, and see the result in your main gui session. 120 | 121 | ![ConnectBot settings](./pics/settings.png) 122 | 123 | ##### install and run my typing programme 124 | 125 | Don't actually do this. 126 | 127 | ``` 128 | $ cd [project directory that you cloned] 129 | $ make # to generate images and special null file 130 | $ sudo apt-get install python3-gi 131 | $ python3 start.sh aux 132 | ``` 133 | 134 | ##### actually just make your own typing solution 135 | 136 | Check this out: 137 | 138 | ``` 139 | $ screen -S aux -X stuff x 140 | ``` 141 | 142 | An `x` should appear in your terminal 143 | 144 | You need to somehow make all the control characters work. The ones you need 145 | 146 | ``` 147 | infocmp screen 148 | ``` 149 | 150 | Look at [my program](./start.py) for ideas on how to implement yours that is 151 | even better 152 | 153 | There is some more debugging advice further down. 154 | 155 | ##### create a services or something 156 | 157 | That would be cool to have the ssh daemon start the the typing program as a 158 | service whenever the ssh session is opened - like similar to what you can do 159 | with udev. 160 | 161 | I think you have to do something with `ForceCommand` in `/etc/ssh/sshd_config` 162 | 163 | I think you can also watch `/var/log/auth.log` for `sshd` and `Accepted` using 164 | awk. 165 | 166 | ## FAQ 167 | 168 | #### How well does it work? 169 | 170 | It works really well on my Kindle Fire at the office as a terminal. It is too 171 | slow at home, with my iPad, as my emacs screen. This could be because: 172 | * the ssh client that I found that works with my old iPad is too slow to update 173 | after characters get stuffed in 174 | * the wifi network at home is too slow 175 | * trying to use it as a full text editor for serious work makes the speed more 176 | noticeable 177 | (I have used emacs at the office set-up in the picture, and it seems to work 178 | pretty well, but it's too small to really try to work with it - I am also 179 | thinking using emacs to display my email inbox with it so I can keep an eye on 180 | my email while I work) 181 | 182 | I am planning to mainly use it at the office to tail logs, but I have been 183 | 'daily driving' it for all terminal stuff just to try it out. 184 | 185 | ConnectBot on Kindle Fire doesn't make it easy to get into landscape mode. 186 | [Here](https://github.com/connectbot/connectbot/issues/868) it sounds like you 187 | have to connect a Bluetooth keyboard. That's actually an interesting idea - I 188 | could have made my type-into-the-terminal app act like a bluetooth keyboard for 189 | your tablet instead of inserting characters into the screen session. 190 | 191 | Originally, I had hoped for landscape, but now I have found a great for my 192 | portrait tablet between my monitors, and found that for small terminal windows, 193 | splitting across is good, and for logs, long is good. The text has to be quite 194 | small to get 80 characters across. 195 | 196 | I got used to finding the application with Alt+Tab. I tried to drag my mouse 197 | over to the tablet a couple times. I don't really use workspaces. I like to be 198 | able to see as many things as possible by turning my head, and find applications 199 | to bring to the foreground with Alt+Tab. If you use workspaces, I guess it 200 | changes things a little bit. You could search for the terminal typing app, and 201 | hide your other windows. On GNOME 3, or at least on Ubuntu 22.04, on my 202 | computer, one of the screens never changes with workspace, so I guess the 203 | terminal typing app could go in there. On MATE, both screens are in the 204 | workspace, but I think there is an always in active workspace function 205 | somewhere. Just brainstorming though - I am sure if a workspace user wants to 206 | do the same thing, they'll work it out. 207 | 208 | #### What about paste? 209 | 210 | Ctrl+Shift+V for paste works by stuffing your whole clipboard into the screen 211 | session. 212 | 213 | #### What about copy? 214 | 215 | I haven't figured out copy yet. I think there must be an easy way to send my 216 | tmux clipboard to the graphical clipboard. Since I start `screen` with 217 | `DISPLAY=:0`, any copy to clipboard utility should hopefully "just work". 218 | 219 | #### Why do you use `screen` _and_ `tmux`? 220 | 221 | * I didn't want all the tmux chrome when using emacs 222 | (you can turn it off with tmux, but then that changes the tmux experience when 223 | I do want to use tmux) 224 | * I got the stuff command working with screen, and it's just a MVP/POC so I left 225 | it like that. 226 | 227 | #### If you are an emacs user, shouldn't you use one of the emacs shells? 228 | 229 | Yeah I never really got into that, and I always have a full screen worth of 230 | open text files. 231 | 232 | #### Why don't you just use ____? 233 | 234 | I have to admit, I didn't look to hard to see if there was a ready to go way to 235 | do this - if somebody knows of one, please just submit a pull request that 236 | deletes my whole repo, and leaves only a readme that points me toward the 237 | alternative. 238 | 239 | ## Developing the typing application. 240 | 241 | For developing, and getting the keycodes right, you don't really need a tablet. 242 | 243 | I start a screen session in an ordinary terminal instead of the tablet, and 244 | then run this program: 245 | 246 | ``` 247 | import sys 248 | import termios 249 | import tty 250 | 251 | old_settings = termios.tcgetattr(sys.stdin) 252 | try: 253 | tty.setraw(sys.stdin.fileno()) 254 | while True: 255 | char = sys.stdin.read(1) 256 | print("%s\r" % hex(ord(char))) 257 | if char == '\x03': 258 | raise KeyboardInterrupt 259 | finally: 260 | termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) 261 | ``` 262 | 263 | Then I connect my python typer program to that screen session, and compare the 264 | codes that I get when I type straight into the terminal to the codes I get when 265 | I get when I use my typing application. 266 | 267 | Hopefully after reading 268 | [this](https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios/) I 269 | will understand more about what `tty.setraw` does. 270 | --------------------------------------------------------------------------------